<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>DaraW</title>
  
  <subtitle>Code is Poetry</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://blog.daraw.cn/"/>
  <updated>2026-03-28T15:41:48.518Z</updated>
  <id>https://blog.daraw.cn/</id>
  
  <author>
    <name>DaraW</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>GMTC 北京 2021 小程序开发实践专场所见所想</title>
    <link href="https://blog.daraw.cn/2021/08/06/gmtc-beijing-2021-mp/"/>
    <id>https://blog.daraw.cn/2021/08/06/gmtc-beijing-2021-mp/</id>
    <published>2021-08-06T11:06:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>七月初有幸参加了 GMTC 北京 2021 小程序开发实践专场，一共听了四个演讲主题，分别是：</p><ul><li>京喜跨端小程序开发实践</li><li>智能生成云端一体代码，提升小程序开发效率</li><li>滴滴出行基于 Mpx 的复杂小程序解决方案</li><li>砥砺前行—抖音小程序前端渲染框架演进</li></ul><p>在密集的听完一下午的分享后，感觉信息量还是比较大的，引发的思考更是不少，下面就慢慢道来。</p><a id="more"></a><h1 id="京喜跨端小程序开发实践"><a href="#京喜跨端小程序开发实践" class="headerlink" title="京喜跨端小程序开发实践"></a>京喜跨端小程序开发实践</h1><p>第一场是京东旗下京喜产品的前端工程师带来的分享，主题围绕小程序和 H5 跨端开发展开，恰巧的是我们目前也在做类似的事情，所以这一场听的格外仔细。</p><h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>京喜作为京东的社交电商产品（对标拼多多），由于又得到了微信的「发现页」流量入口，所以业务体量比较大：</p><ul><li>整包体积 20M</li><li>100+ 开发者，且分了不同的团队</li><li>峰值亿级请求量</li><li>跨多端：多个平台的小程序 + 多个容器内的 H5</li></ul><h2 id="跨端实践"><a href="#跨端实践" class="headerlink" title="跨端实践"></a>跨端实践</h2><p>整个京喜小程序跨端方面基于京东自研的 Taro 框架，比较值得关注的是京喜由于体积大、存在历史遗留问题，每个页面就是一个仓库，可以针对单页进行编译开发、多端调试，单页构建完成后最终会再合并构建到整个项目中，相当于会做二次构建。另外针对这个架构，整个京喜小程序内存在着多种技术栈。</p><p>在跨端语法这块，基于 Taro 的跨端开发能力，详细可以参考 <a href="https://taro-docs.jd.com/taro/docs/envs" target="_blank" rel="noopener">https://taro-docs.jd.com/taro/docs/envs</a> ，这里不再赘述：</p><ul><li>内置环境变量</li><li>条件编译</li><li>多端文件后缀</li></ul><h2 id="首页性能优化"><a href="#首页性能优化" class="headerlink" title="首页性能优化"></a>首页性能优化</h2><p>讲师经历了首页性能优化项目并取得了一定成果，在这之前京喜首页面临了一些性能瓶颈：低端机不流畅、机器发热等问题。<br>首页性能优化一共从三点展开来做：虚拟列表、低端机降级、体验评分。</p><h3 id="虚拟列表"><a href="#虚拟列表" class="headerlink" title="虚拟列表"></a>虚拟列表</h3><p>虚拟列表的落地场景是首页的商品列表，商品列表的特点是长列表、功能复杂，这就导致了滚动两屏后容易卡顿，所以做了虚拟列表的落地：</p><p><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/45fcc4bd-04f6-4b5e-b576-cc1b76ff3d1a.png" alt=""></p><ul><li>结构改造<br>虚拟列表常见的一个场景是整个页面长列表，但这里比较特殊，不是整个页面都是列表，而是只有页面的局部。所以这里组件本身不太好去监听页面的 scroll 事件，只能基于 Taro 的事件中心从组件外传进来滚动事件。</li><li>卡片高度计算<br>商品卡片的一个特点是不定高，直观的思路就是通过 DOM API 去获取卡片 DOM 的高度，但这里有个问题是小程序中的 DOM API（SelectorQuery）性能不太好，在这样的列表场景下大量去调用更加容易遇到性能问题。<br>既然 DOM API 不可取，那可以从业务入手，可以发现卡片的高度可以根据内容计算出来，把各个组成部分的高度加起来即可。这个思路确实可行而且性能很好，但还是遇到了一个小问题：小程序计算出来的高度和实际 DOM 高度不一致，经过排查后发现是 rpx 换算规则导致高度不符合预期，1rpx 在绘制的时候会转换成 0.42px，而在计算的时候会转换成 0.426667px，积少成多这里就可能会带来偏差。解决办法也很简单，计算逻辑针对小程序平台做一下区分即可。</li></ul><p><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/7aee8f83-0139-455a-8082-0bd70844325d.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/f02bf3ee-7418-483a-a979-1f3371034fd7.png" alt=""></p><ul><li>白屏优化<br>至于白屏优化则比较简单，是常见的骨架图，这里不再赘述。</li></ul><h3 id="低端机降级"><a href="#低端机降级" class="headerlink" title="低端机降级"></a>低端机降级</h3><p>低端机降级首先是划分标准，这里一开始研发先定了一个标准，根据年份、系统、上市时间来判断是否是高/低端机。但落地后发现高端机标准定低了，有些高端机也会卡顿，以及高端机的比例和产品渠道的用户划分比例也不符合。在重新校准参数后，最终大约 2/3 为高端机，1/3 为低端+未知，比较符合事实。<br>在明确低端机的标准后，针对低端机就可以做一些优化了：</p><ul><li>轮播图改单图</li><li>动图改静图</li><li>大图改小图</li><li>去除跨模块/时间关联（简化弹窗等逻辑）</li></ul><h3 id="体验评分"><a href="#体验评分" class="headerlink" title="体验评分"></a>体验评分</h3><p>小程序 IDE 自带了一个体验评分，其指标主要由以下几点组成：</p><ul><li>首屏 DOM 数（控制在 1000）</li><li>图片大小</li><li>setData 调用频率、内容大小<br>京喜针对这些优化项，将体验评分做到了全 100 分。</li></ul><h3 id="代码体积优化"><a href="#代码体积优化" class="headerlink" title="代码体积优化"></a>代码体积优化</h3><p>小程序有着包体积限制，超出则无法发版，所以需要优化代码体积。<br>常见的收益较大的方式：</p><ul><li>分包</li><li>依赖分析，移除未使用的内容</li><li>避免使用本地资源尤其是图片</li><li>所有类型都进行压缩和注释清理</li></ul><p>除此之外，京喜首页还针对 CSS 做了一些更加精细化的优化：</p><ul><li><p>继承属性：<br>有些 CSS 属性只要在全局定义过之后，里面是可以不用再重复定义的，例如 <code>color</code> <code>line-height</code> <code>font-family</code> <code>font-size</code> 以及 <code>@font-face</code> 字体定义。这里讲师举了个例子，在他们的首页项目中 <code>@font-face</code> 重复定义了 231 次。经过了这一系列优化后，首页模块体积一共减少了 10K，占项目总量 630K 的 1.6%。</p></li><li><p>样式补全：<br>例如 <code>display: flex</code> 在最终页面中往往会通过各种方式再加上一行 <code>display: -webkit-flex</code> 以便支持多个宿主环境。<br>在小程序 IDE 中，在上传代码的时候支持选择样式自动补全，当然框架一般也会通过 PostCSS 在编译阶段补全。那么到底该选择哪个呢？经过京喜的对比尝试，发现小程序 IDE 自带的补全在使用上没啥问题，同时还可以避免 PostCSS 编译加入的样式补全代码的算进小程序的体积里，所以最终关闭了框架的补全，选择使用了小程序 IDE 的补全，这样使得包体积减少了 58K，占项目总量 630K 的 9%。</p></li><li><p>样式命名优化<br>JS 代码在编译的过程中 UglifyJS 可以将函数名简化成 a b c 这样；那么同理我们可以把组件内的 classname 进行优化。<br>默认情况下 CSS 和 JSX 分离，很难把 JSX 中的 classname 和 CSS 中的 classname 建立关联。虽然可以做，但比较麻烦。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/6cfa979e-56cd-4fad-a20b-880edff0e1dd.png" alt=""></p></li></ul><p>所以京喜选择了 CSS Module 来建立样式和 JS 关联：<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/bf29be96-701a-4517-aca2-267e9852bda9.png" alt=""></p><p>这样优化后，体积又减少了 50K，占项目总量 630K 的 7.9%。</p><p>这一系列的优化使得体积从 631K 减少到了 465K，在小程序包体积寸土寸金的背景下，效果是比较显著的。</p><h2 id="系统监控"><a href="#系统监控" class="headerlink" title="系统监控"></a>系统监控</h2><p>京喜小程序借助了 Badjs 来做质量数据收集，但会遇到一些问题，线上质量数据数量大、模块划分不明显、实时反馈信息少。因此建立了一些可用率监控平台，配合数据，建立环比、同比等规则进行报警处理。</p><h2 id="Q-amp-A-环节"><a href="#Q-amp-A-环节" class="headerlink" title="Q&amp;A 环节"></a>Q&amp;A 环节</h2><ul><li>为什么首页用 Taro 其他页面可以用其他的技术栈<ul><li>首页是实验田，其他业务会有自己的选型或者历史遗留问题</li><li>首页单页编译，避免其他的业务影响，只对首页编译</li></ul></li><li>评分卡中什么页面是小程序评分比较好的，依据来源<ul><li>参考的 Google Chrome 的</li></ul></li><li>灰度策略<ul><li>开发者灰度体验包</li><li>逐步放量</li></ul></li></ul><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><h3 id="业务"><a href="#业务" class="headerlink" title="业务"></a>业务</h3><p>这场演讲是业务角度的分享，目前我们所做的某个细分方向业务中，主推的也是一个跨多端（微信内 H5 和微信小程序）、包含多个业务模块的产品，京喜的分享给我们指明了一些未来发展的道路。</p><h3 id="跨端"><a href="#跨端" class="headerlink" title="跨端"></a>跨端</h3><p>在跨端实践这块，Taro 提供的方式已经足够我们很好的去做多端开发与调试，以及代码的维护。<br>虽然 Taro 跨端开发的学习成本很低，但特别是新同学加入后，还是会有一个短暂的适应期，早期在人不多的时候，我们往往会在 CodeReview 阶段去避免不符合跨端最佳实践的代码进入最终的 codebase，但现在团队人员规模逐渐增加，我们尝试通过 Linter Plugin 来沉淀我们在过去的 CodeReview 中积累的跨端经验，目前已经初步在项目中落地了，且确实有所见效，统计 CodeReview 数据发现落地后跨端语法类的问题<strong>下降了 90+%</strong>，对于这块未来我们会做更多的详细的分享。<br>因此在这点上，我们可以比较自信的说做的是比京喜好的，至少我们 CodeReview 比较严格，没有京喜首页模块重复定义了 200+ 次字体文件这样的问题。未来我们也会继续迭代 Linter Plugin，将更多的实践沉淀到其中，将问题扼杀在编写代码的过程中。</p><h3 id="构建"><a href="#构建" class="headerlink" title="构建"></a>构建</h3><p>在小程序包体积的方面，我们已经即将超出 2M 的单包体积限制。这块京喜给我们指了一条道路，也是 Q&amp;A 环节大家都比较感兴趣的一点，京喜在构建上做了类似客户端团队的拆分工作。<br>像今日头条、抖音、支付宝等这类包含了多个业务逻辑的巨型 App，往往会跨团队研发，在开发中启动整个 App 的所有模块肯定是不现实的，所以一般都会去做 App 功能模块的独立开发与构建。京喜小程序应该是将项目分成了多个独立分包，所以可以实现单个模块的独立开发和构建，最终再将各个模块再合并构建一次产出最终的小程序。<br>不同于京喜各个页面的跨端是独立的，H5 应用各个页面是分离的，我们整个应用也提供了 H5 形式的 SPA，所以我们也不能完全照搬京喜的构建经验。<strong>下半年我们将重点探索 Taro 框架下完美支持跨端的分仓库构建，提升开发体验和效率。</strong></p><h3 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h3><p>上半年我们业务上经历了从无到有的过程，业务的起量也是最近几个月刚开始爆发，所以在过去的半年内，我们主要围绕着研发提效等角度去做技术规划，性能方面没有遇到比较大的问题，所以还未曾特别关注过性能数据。预计下半年业务稳定后，我们将会在性能方面去做一些优化工作，京喜的经验将是我们的重点参考资料，例如低端机降级等。</p><h3 id="监控"><a href="#监控" class="headerlink" title="监控"></a>监控</h3><p>小程序像 Web、却也像 App，字节跳动内端端监控目前针对小程序的支持其实是不够完善的，和 Web + App 相比不少监控指标缺失。做一做基本的监控和报警还可以，但想细化数据却比较难。下半年我们在性能优化的工作中，将尝试能不能积累一些跨端的性能优化经验，并帮助端监控完善这块。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这场分享是四场中和业务开发最贴近的一场，也是四场中引发我的思考最多的一场，会后和讲师做了一些简单的交流，并加了讲师的微信，后续也会和讲师做进一步的技术交流，学习友商团队的更多经验。</p><h1 id="智能生成云端一体代码，提升小程序开发效率"><a href="#智能生成云端一体代码，提升小程序开发效率" class="headerlink" title="智能生成云端一体代码，提升小程序开发效率"></a>智能生成云端一体代码，提升小程序开发效率</h1><p>这一场是 DCloud 的 CTO 做的分享，主要讲小程序 PaaS 相关的内容。</p><h2 id="前-后端协同模式演变"><a href="#前-后端协同模式演变" class="headerlink" title="前/后端协同模式演变"></a>前/后端协同模式演变</h2><p>讲师将前后端协同模式划分了几个阶段：</p><ul><li>第一阶段：1990~2005 年，HTML 诞生后，页面主要由 ASP/JSP/PHP 等模板序言实现的动态网页+ JS + CSS 组成</li><li>第二阶段：2005~2018 年，AJAX 的出现和移动互联网的成熟，使得各个端合在一起变成了大前端，WebApp、原生 App、小程序等提供了较好的用户体验，但开发效率比较低。在这个阶段，跨端逐渐成为主流，例如 ReactNative、Weex、Taro、uni-app、Flutter 等技术方案，跨 H5、iOS、Android、小程序多端。</li><li>第三阶段：2018~今天，Serverless 的出现，使得开发者可以不需要搭建服务器，在端上直接操作数据库，在保证用户体验的同时提升了开发效率，演变成了重前端、轻后端的架构，业务逻辑迁移，前端承担更多的责任。</li></ul><h2 id="小程序云开发的先进性和局限性"><a href="#小程序云开发的先进性和局限性" class="headerlink" title="小程序云开发的先进性和局限性"></a>小程序云开发的先进性和局限性</h2><ul><li>表级权限，场景受限制，稍微复杂一点的规则就没法做了<ul><li>积分大于50的注册用户，才能评论文章</li><li>仅管理员可设置精华帖，阅读量系统自增</li></ul></li><li>参数合法：落库的时候难做数据校验</li><li>数据割裂：小程序厂商支持程度和标准都有差别，用了多家的云就会导致数据分散在各个云数据库中，难以一起统计</li></ul><h2 id="uniCloud-的改进探索"><a href="#uniCloud-的改进探索" class="headerlink" title="uniCloud 的改进探索"></a>uniCloud 的改进探索</h2><ul><li><p>跨云跨端：uniCloud SDK + uni-app runtime<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/9929d3b6-b181-43dd-aa08-6190d06d7370.png" alt=""></p></li><li><p>规范制定：借助 JSON Schema 支持权限配置、字段校验、组件绑定，设计了一套 DSL 来做权限控制和字段校验<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/3e11ca9f-0038-402a-bbbd-290bca8d62c5.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/3c0f3149-6014-4ef8-9df0-0c2f28349f24.png" alt=""></p></li><li><p>clientDB 云端一体化：前端 DB SDK + 数据组件<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/20b8612f-2911-4e5e-b2b4-8e401453c162.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/3c1cfaad-54b0-4159-b569-c88bd8b428c7.png" alt=""></p></li></ul><h2 id="智能生成小程序代码-Schema2Code"><a href="#智能生成小程序代码-Schema2Code" class="headerlink" title="智能生成小程序代码(Schema2Code)"></a>智能生成小程序代码(Schema2Code)</h2><p>大家大多接触过类似的 LowCode 方案，这里不详细讲了，只贴一张图，更多的可以看下 PPT。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/340fc5b3-b0c1-488c-a308-5cb29ff8f58d.png" alt=""></p><h2 id="未来的轮子生态"><a href="#未来的轮子生态" class="headerlink" title="未来的轮子生态"></a>未来的轮子生态</h2><p>在当下前后端分离导致前后端轮子生态割裂，前端框架和后端框架之间有着很大的沟壑；在前端承载越来越多职责的背景下，未来可能会往云端一体化的方向去发展。<br>但目前的云开发虽然高效，但局限很大，难以满足复杂的业务，未来探索的方向包含两个：对复杂业务场景的更好支持（跨云跨端、精细化权限、严格字段校验、前端数据组件）和基于云开发模式下的代码生成思路。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/5f996f7d-011f-457b-bbca-0cf5e5a43fcf.png" alt=""></p><h2 id="Q-amp-A-环节-1"><a href="#Q-amp-A-环节-1" class="headerlink" title="Q&amp;A 环节"></a>Q&amp;A 环节</h2><ul><li>云端 MongoDB 数据库可能是低版本不支持事务，那么怎么实现事务<ul><li>云端厂商版本选用高版本</li><li>云端做事务，前端做不了事务</li></ul></li></ul><h2 id="思考-1"><a href="#思考-1" class="headerlink" title="思考"></a>思考</h2><p>注意 DCloud 同时也是开源 uni-app 的公司，uni-app + uniCloud 这种云端一体化的发展方向也算是 DCloud 这种 PaaS 云服务厂商在当下的最佳方案。<br>在这块由于我没有怎么关注过，也没有落地的经验，所以不献丑了。公司内对应的最适合做这种方案的团队应该就是轻服务了，轻服务和 uniCloud 端 SDK 的使用方式也确实很相似，只是轻服务是 Web 体系内的，uniCloud 是小程序体系内的。</p><h1 id="滴滴出行基于-Mpx-的复杂小程序解决方案"><a href="#滴滴出行基于-Mpx-的复杂小程序解决方案" class="headerlink" title="滴滴出行基于 Mpx 的复杂小程序解决方案"></a>滴滴出行基于 Mpx 的复杂小程序解决方案</h1><p>这一场是滴滴的高级专家工程师分享他们开源的 Mpx 小程序框架。<br>滴滴出行小程序复杂性介绍</p><ul><li>复杂的业务逻辑(多场景/多状态/重地图)</li><li>庞大的业务体量(18+业务线/营销业务闭环/总包 体积20M)</li><li>跨团队合作开发(10+业务团队)</li><li>跨多端投放(微信/支付宝/QQ/字节/Web)</li><li>多语言支持(支持中英文切换)</li></ul><h2 id="Mpx-小程序框架概览"><a href="#Mpx-小程序框架概览" class="headerlink" title="Mpx 小程序框架概览"></a>Mpx 小程序框架概览</h2><h3 id="小程序优先的增强型跨端开发框架"><a href="#小程序优先的增强型跨端开发框架" class="headerlink" title="小程序优先的增强型跨端开发框架"></a>小程序优先的增强型跨端开发框架</h3><p>不同于业内主流小程序框架追求将 Web MVVM 框架（React/Vue）运行在小程序环境中 的思路，Mpx以小程序原生的语法和技术能力为基础，借鉴参考了 Vue 框架中的优良语法设计，通过编译和运行时手段对其进行增强扩展，让用户能够以接近 Vue 的体验和方式来进行小程序开发，并保持与原生小程序一致甚至更优的性能与包体积。 </p><h3 id="业内小程序框架异同对比-性能与效率的权衡"><a href="#业内小程序框架异同对比-性能与效率的权衡" class="headerlink" title="业内小程序框架异同对比 - 性能与效率的权衡"></a>业内小程序框架异同对比 - 性能与效率的权衡</h3><p><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/26ffe296-db43-427c-b486-0ed8f885fd6d.png" alt=""><br>目前业界比较常见的两种框架类型是静态编译型和动态渲染型：</p><ul><li>静态编译型以 uni-app 和 Taro2 为代表：<ul><li>DSL 层 uni-app 为 Vue，Taro2 为 React</li><li>技术方案则是重编译期，将 Vue / React 直接编译到小程序原生</li><li>优点是性能比较好，因为编译后的代码接近原生写的小程序；且能够具备 Web 迁移能力</li><li>缺点也比较明显，因为通过编译实现，Web 框架使用能力受限，例如 Taro2 中 JSX 循环限制很大，容易写出 bug</li></ul></li><li>动态渲染型，运行时框架，目前的一种趋势，以 Taro3 为代表：<ul><li>DSL 层可以随意使用任何 Web 框架</li><li>技术方案是重运行时，借助递归动态模板模拟 DOM 环境，所以可以直接将 Vue / React 等视图层的框架直接跑在小程序上</li><li>优点和静态编译型相反：Web 框架的使用不受限制，可以自由使用几乎是所有的语法，因此迁移 Web 时也很容易</li><li>缺点也和静态编译型相反：性能相对来说差一些，setData 发送数据的开销和渲染的开销都相对大些</li></ul></li></ul><p>这两类实现方式因为以 Web 的方式去做小程序开发，使用部分小程序原生特性也会有些麻烦。</p><p>Mpx 则走了第三条路，原生增强型：</p><ul><li>DSL 层基于小程序的语法去做拓展，因为本身语法就类似 Vue，所以拓展后是类 Vue 的语法，所以编译</li><li>技术方案也和 Vue2 相似，编译+运行时</li><li>优点时性能可以做到极佳，因为 Vue 在编译期可以做 AOT，而运行时又有精细化的依赖追踪，所以可以保证 setData 时的更新细粒度几乎是最佳；同时由于是小程序的语法拓展，所以和原生小程序开发体验也比较接近，原生语法的使用不是很麻烦</li><li>缺点则是和小程序靠拢过紧，不是完整的 Vue，Web 迁移能力比较弱</li></ul><p>总结起来就是 Mpx 在设计的时候做了一些权衡，牺牲了一些 Web 开发的效能，换取更高效的小程序性能。</p><h3 id="Mpx-全局架构"><a href="#Mpx-全局架构" class="headerlink" title="Mpx 全局架构"></a>Mpx 全局架构</h3><p>和业界常见的框架类似，分为编译层和运行时。编译层完全基于 Webpack，通过深度定制的 Webpack Plugin 来实现编译；在运行时核心模块包含小程序平台差异抹平、响应式逻辑等，同时还包含了一些周边库，例如 Mock 等。</p><h3 id="Mpx-支持能力概览"><a href="#Mpx-支持能力概览" class="headerlink" title="Mpx 支持能力概览"></a>Mpx 支持能力概览</h3><ul><li>开发体验与效率<ul><li>数据响应</li><li>跨端开发</li><li>模板指令增强</li><li>CSS 预处理</li><li>ES6+ 支持</li><li>TypeScript 支持</li><li>i18n支持</li></ul></li><li>质量与性能<ul><li>setData 优化</li><li>按需构建</li><li>分包资源处理</li><li>包体积优化</li><li>包体积分析</li><li>单元测试支持</li><li>SourceMap 支持</li></ul></li><li>跨团队开发<ul><li>npm 支持</li><li>多实例 Store</li><li>Packages 规范</li><li>原生渐进迁移</li></ul></li></ul><h3 id="Mpx-使用情况"><a href="#Mpx-使用情况" class="headerlink" title="Mpx 使用情况"></a>Mpx 使用情况</h3><p>Mpx 支持了滴滴内部几乎所有的小程序业务，例如滴滴、青桔、橙心优选等，并在 GitHub 上获得了 2.8K Stars。</p><h2 id="数据响应与性能优化"><a href="#数据响应与性能优化" class="headerlink" title="数据响应与性能优化"></a>数据响应与性能优化</h2><h3 id="数据响应编程"><a href="#数据响应编程" class="headerlink" title="数据响应编程"></a>数据响应编程</h3><p>类似 Vue 的语法，这里不再赘述。</p><h3 id="Mpx-数据响应实现"><a href="#Mpx-数据响应实现" class="headerlink" title="Mpx 数据响应实现"></a>Mpx 数据响应实现</h3><p>Mpx 参考了 Vue2 的实现，基于 <code>defineProperty</code> 实现了数据响应式。其中和 Vue 的区别在于，Vue 因为是 Web 平台，可以直接操作 DOM，在数据发生变化后最终驱动了 DOM 的变动；而小程序中无法直接操作 DOM，所以 Mpx 中数据发生变动后驱动的是小程序的 setData，由小程序去负责渲染。</p><p>未来当小程序宿主环境兼容性变好了之后，Mpx 也会考虑更换为 Vue3 的 Proxy 来实现。</p><h3 id="数据响应下的-setData-优化"><a href="#数据响应下的-setData-优化" class="headerlink" title="数据响应下的 setData 优化"></a>数据响应下的 setData 优化</h3><p>小程序 setData 性能优化建议一般是三条：避免 setData 的数据过大、避免 setData 的调用过于频繁，和避免将模板中未绑定的数据传入 setData。<br>一般框架的实现有三种：</p><ul><li>全量设置：将整个模板对应的数据传入，这样实现最简单粗暴，性能也是最差的</li><li>深度 diff 设置：将上次和这次的数据做一个深度 diff，进行增量的 setData，这种实现方式可以做到避免 setData 数据过大和调用频繁，但做不到避免将没有用到的数据传入，因为这种情况下数据和模板没有去做关联，是不知道模板里真正需要的数据到底是哪部分的</li><li>只设置模板上使用且发生变化的最小数据：这是 Mpx 的做法，也是性能最好的实现方式</li></ul><h3 id="基于-render-函数的-setData-优化方案"><a href="#基于-render-函数的-setData-优化方案" class="headerlink" title="基于 render 函数的 setData 优化方案"></a>基于 render 函数的 setData 优化方案</h3><p>在 Vue 中，Vue 通过依赖收集和追踪，最终能够做到精细化的 DOM 更新，那么在 Mpx 中类似的，通过依赖收集和追踪，将数据变动改为和 setData 进行关联，可以做到精细化的 setData 更新。<br>同时 Mpx 可以和 Vue 一样通过 nexttick 来合并渲染。</p><h3 id="render-函数生成流程"><a href="#render-函数生成流程" class="headerlink" title="render 函数生成流程"></a>render 函数生成流程</h3><p>第一次编译将模板编译为原始的渲染函数，但这个原始的渲染函数没法直接在运行时使用，所以会基于 Babel 再做二次编译，去关联 this 上下文，便于之后可以直接在运行时使用。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/958a7c79-59b8-4799-86cd-90802c070c06.png" alt=""></p><h3 id="Mpx-setData-优化效果"><a href="#Mpx-setData-优化效果" class="headerlink" title="Mpx setData 优化效果"></a>Mpx setData 优化效果</h3><p>在滴滴使用 Mpx 的项目中，没有手动去做 setData 优化，但性能一直很优秀。Mpx 的性能在 benchmark 场景下也是最优。</p><h2 id="编译构建与包体积优化"><a href="#编译构建与包体积优化" class="headerlink" title="编译构建与包体积优化"></a>编译构建与包体积优化</h2><h3 id="Mpx-编译构建"><a href="#Mpx-编译构建" class="headerlink" title="Mpx 编译构建"></a>Mpx 编译构建</h3><p>由于 Mpx 编译构建基于 Webpack，所以整个依赖分析、编译构建的流程和普通的 Vue 项目相似。</p><h3 id="小程序资源依赖树"><a href="#小程序资源依赖树" class="headerlink" title="小程序资源依赖树"></a>小程序资源依赖树</h3><p>小程序项目虽然看起来资源比较离散，但实际是通过 app.json page.json 来描述了资源依赖关系，在做依赖分析的时候可以从这点入手。</p><h3 id="npm-支持"><a href="#npm-支持" class="headerlink" title="npm 支持"></a>npm 支持</h3><p>微信小程序官方的 npm 支持比较差，有诸多问题例如全量复制 npm 包，Mpx 做了优化，解决了这些问题。</p><h3 id="分包构建"><a href="#分包构建" class="headerlink" title="分包构建"></a>分包构建</h3><p>根据用户分包配置，串行对主包和各个分包进行构建。同时还能自动生成 SplitChunksPlugin 配置，将项目中的公共模块输出到主包或分包 bundle 中。<br>对于独立分包也做了支持，独立分包可以在没有主包的情况下运行，所以在独立分包的场景下会给独立的 runtime 和相关资源，保证其能够独立运行。</p><h3 id="小程序包体积分析"><a href="#小程序包体积分析" class="headerlink" title="小程序包体积分析"></a>小程序包体积分析</h3><p>Webpack 生态中，包体积分析工具都是针对 Web 来设计的，在小程序场景下有一些缺点，拿 <code>webpack-bundle-analyzer</code> 来举例子：</p><ul><li>只能统计分析 js 模块体积，而小程序输出中包含大量非 js 资源</li><li>因为 Web 体系内没有分包的概念，所以无法以分包维度进行体积统计分析，但小程序的体积限制是设置在每个分包上的</li><li>体积来源难以追溯<br>针对这些问题，Mpx 内置了包体积分析工具，支持以下特性：</li><li>全量资源（js 与非 js）体积统计分析</li><li>支持以分包维度进行体积统计分析</li><li>支持通过来源配置分组进行体积统计分析</li><li>支持按照分组和分包维度配置体积阈值进行体积管控</li></ul><p>在滴滴的业务场景下，各个业务方会在一个小程序里进行开发，那么去分析每个业务的体积就非常重要。<br>因此 Mpx 包体积分析设定了 entry 的概念，根据 entry 进行分组。接着从 app.json 开始，对整个依赖树做一次深度优先遍历，将他们的资源大小信息根据分组进行归类。如果这个资源都是这个分组使用的，那就是这个分组的 self size，否则算 shared size。除此之外，滴滴还做了一套可视化的页面，用来展示上面包体积分析得到的数据。</p><h2 id="小程序跨端"><a href="#小程序跨端" class="headerlink" title="小程序跨端"></a>小程序跨端</h2><p>Mpx 1.0 针对各个平台都做了增强，到了 2.0 则以微信小程序作为标准，由框架来进行跨平台转换支持支付宝、百度等小程序平台。</p><p>在设计和实现的时候，针对静态的部分主要通过编译转换来做，针对动态的部分主要通过运行时来抹平差异，对于框架无法处理的差异提供了条件编译的能力，这个和 Taro 等框架类似，有几种写法：</p><ul><li>文件维度条件编译，通过文件后缀来标识平台，例如 <code>card.wx.mpx</code> <code>card.ali.mpx</code></li><li>区块维度条件编译，在单文件组件中，script 标签可以配置 mode 属性，来指定这块逻辑适配某个平台</li><li>代码维度条件编译，通过环境变量来区分 if else，并支持编译时移除条件死分支</li><li>属性维度条件编译，当平台差异只存在于节点属性维度时，使用代码维度条件编译较为冗余，子树难以维护： <code>&lt;view class@wx=&quot;wx-container&quot; class@ali=&quot;ali-container&quot;&gt;&lt;/view&gt;</code></li></ul><p>在输出 Web 平台方面，因为语法借鉴了 Vue，所以主要还是基于 Vue 来实现。在编译时将小程序基础能力例如页面路由能力的实现注入进去。其中组件这块，Mpx 直接将 view、text 等标签换成了 HTML 中的基础组件，例如 view 换成了 div。</p><h2 id="Mpx-多语言支持"><a href="#Mpx-多语言支持" class="headerlink" title="Mpx 多语言支持"></a>Mpx 多语言支持</h2><p>由于小程序是双线程架构，应该尽量减少 setData 数据传输，所以 Mpx i18n 方案有两种，一种是 wxs 模式，把多种语言文案放在模板中，这种方案没有额外的通信开销，性能比较好，但问题是会导致包体积增大，语言包同时存在于 wxs 和 js 中，且因为文案在编译期就决定了，后续无法异步加载；另一种方案是 computed 模式，优缺点与 wxs 模式刚好相反，通信开销较大，但包体积占用较小，且可以实现异步加载语言包。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/830f1dd3-80d9-4fb9-95b5-c3e4b8bd0fc2.png" alt=""></p><h2 id="展望与未来"><a href="#展望与未来" class="headerlink" title="展望与未来"></a>展望与未来</h2><ul><li>Webpack5 构建升级，大幅提升构建速度</li><li>Vue3 数据响应升级，支持 composition api</li><li>单元测试进一步完善，完整支持 jest mock</li><li>E2E 自动化测试支持</li><li>Mpx-cube-ui 跨端组件库开源</li><li>支持局部组件运行时渲染</li></ul><h2 id="Q-amp-A-环节-2"><a href="#Q-amp-A-环节-2" class="headerlink" title="Q&amp;A 环节"></a>Q&amp;A 环节</h2><p>抱歉这里记不太清了。</p><h2 id="思考-2"><a href="#思考-2" class="headerlink" title="思考"></a>思考</h2><p>在分享我的思考之前，先声明下利益相关，我是 Vue Contributor &amp; Taro Contributor，所以<strong>不保证接下来的内容不会夹杂私货</strong>。<br>说实话刚开始听还是有些失望的，本来预期是和第一场类似，主要从业务落地角度讲滴滴出行的小程序实践，没想到没讲几分钟，就变成了一个纯 Mpx 技术细节的分享。纯技术细节的分享更适合以文章的形式展开，以演讲的形式提供很容易导致听众跟不上思路，好在 PPT 内容做的还是比较详细的，倒也不是太大的问题。<br>在听完整个分享后，虽然根据我们的业务背景和团队相关同学的背景，我依然不会去选择使用 Mpx，但 Mpx 的一些工作也给我们指引了一些未来的道路，所以这场倒也不算白听了。</p><h3 id="小程序框架设计漫谈"><a href="#小程序框架设计漫谈" class="headerlink" title="小程序框架设计漫谈"></a>小程序框架设计漫谈</h3><p>分享中虽然针对业内的小程序框架设计做了不少点评，但我这边还是想去展开聊聊。<br>Web 发展至今，HTML、CSS、JS 基本已经成了<strong>事实汇编语言</strong>，只要是构建稍微有一定复杂度的页面应用，几乎不会再有人去拿三剑客从头写了，大家都会选择一个像 Vue、React 之类的 Web 框架。<br>小程序中三剑客的设计尽管和 Vue 极其相似，但还是逃不掉成为事实汇编语言的命运，当然这背后的原因是很多的，DSL 设计的合理性暂且不谈，程序员永远都是懒惰的，如果能用熟悉的 Web 框架去开发，谁又愿意去学习一套新的语法呢？这也是小程序框架例如 uni-app、Taro 出现的背景，甚至连微信小程序官方都在推荐 Kbone 这样的框架。</p><h4 id="重编译型框架"><a href="#重编译型框架" class="headerlink" title="重编译型框架"></a>重编译型框架</h4><p>在第一代小程序框架中，大家的设计思路都是既然小程序语法和 Vue、React 像是吧，那就干脆把 Vue、React 编译到小程序吧。<br>这种方案最直接的问题就是 Vue、React 的语法是针对 Web 平台的，在小程序中很多使用会受限，例如以 Vue 语法为代表的 mpvue 中无法使用复杂的 JS 表达式，以 React 为代表的 Taro 1/2 中 JSX 里无法使用复杂的 map 循环。<br>导致这些问题的原因基本都是编译成本的问题，简单场景下的 Vue template 和 React JSX 都可以对等的编译到小程序的模板，但复杂语法的支持工作量是非常爆炸的，特别是 JSX 灵活性本质就是 JS，将标准的 JSX 编译到 template 几乎就是在对 JS 做 AOT 的工作量。<br>这里多提一嘴，有一个叫 solid.js 的框架，对 JSX 做了 AOT，当然它的 JSX 的语法使用是受限的。感兴趣的同学可以自行阅读其文档。<br>在这类框架中，最终编译出来的小程序代码和原来写的 Vue、React 代码都是很相似的，自然的性能就和小程序原生很贴近。</p><h4 id="重运行时框架"><a href="#重运行时框架" class="headerlink" title="重运行时框架"></a>重运行时框架</h4><p>大家不想再使用受限的 Web 框架语法，但小程序又不让直接碰 DOM，那怎么办呢？<br>这时有人天才般的发现了「动态递归模板」技术，最早是谁想出来的这里不去追溯了，意义不大，但这个技术本身比较有意思，这里展开聊下。<br>小程序模板往往支持 template 递归引用，例如用下面这段伪代码就可以做到动态的 view 里面嵌套 view：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span> <span class="attr">name</span>=<span class="string">"view"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">block</span> <span class="attr">a:for</span>=<span class="string">"&#123;&#123;item.children&#125;&#125;"</span> <span class="attr">key</span>=<span class="string">"&#123;&#123;item.id&#125;&#125;"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">template</span> <span class="attr">is</span>=<span class="string">"view"</span> <span class="attr">data</span>=<span class="string">"&#123;&#123;item: item&#125;&#125;"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">block</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure><p>那么就有人想到了，是不是可以做一个万能模板，这个万能模板能够描述一切的页面，模板和 JS 之前的 data 就是 VDOM 的 AST？<br>如果大家有使用 Taro3 的项目，可以看下编译产物的目录，base.wxml 就是这个万能模板，而小程序 IDE 中的 AppData 就是页面的 AST：<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/4a77a528-6b79-4b80-a1bd-db56afe175dd.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/949b817d-56c0-4088-a84c-ac5b43be2257.png" alt=""></p><p>这就很舒服了，可以模拟出一切 DOM 操作。<br>这种方案很多人都会说性能不太好，这里也详细聊下，为什么会说性能不好：</p><ol><li>data 是整个页面的 AST，而小程序是双线程模型，分为逻辑层和渲染层（详细可以阅读这个系列的文章 <a href="https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html），也正是这种模型限制了" target="_blank" rel="noopener">https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html），也正是这种模型限制了</a> DOM 操作，只允许将可序列化的页面数据从逻辑层发给渲染层，也就是小程序中的 setData。大家应该都知道，线程间的通信是有开销的，那么 data 越大开销就越大。尽管这种实现方式可以做到合并更新，但 data 包含了 AST，data 大小必定不会小。</li><li>不管用不用到那么多节点，<code>base.xml</code> 永远都是那么大，就像打车的起步价一样，对于轻量级的场景，这是比较吃亏的，这个模板就已经 63K 了。<br>综上，尤其是 benchmark 场景下，动态模板的框架很难在重编译的框架面前占优势。</li></ol><p>但注意，<strong>业务往往和 benchmark 不一样，除了框架开发者和新手之外很少有人没事去写 Todo MVC</strong>。尽管动态模板的方案有起步价，但大家再去观察各个页面编译出的模板文件，会发现里面都是直接引用的 base.xml，<strong>新增一个页面模板这块的体积增加的开销几乎为 0</strong>（在打包的时候还能被 Gzip 压缩掉），因此只要你的页面复杂度和数量达到一定阈值，体积上的缺点会反过来变成优点，就像我们的产品目前页面的数量已经差不多上百了，还有大量的组件，在体积上选择运行时的框架不一定比重编译的框架差。</p><h4 id="再谈-Mpx"><a href="#再谈-Mpx" class="headerlink" title="再谈 Mpx"></a>再谈 Mpx</h4><p>这里不去评价 Mpx，毕竟人家已经说了，是根据他们的业务背景而设计的，他们有一些业务是原生的小程序，Mpx 作为增强型的框架，迁移成本必定比前两者小，但我也很难去称它为第三代的框架。<br>Mpx 给我们的思考主要是两点：<br><strong>一是造轮子要考虑业务背景，这也是讲师作为 Mpx 核心开发者强调的一点。</strong><br>就像去年我们团队内做了一个叫 RingJS 的 Node.js 框架并顺利落地了（这点未来有机会再详细谈谈），其中一个重要的点在于 RingJS 在我们中后台都是裸 Koa Node.js 应用的背景下，迁移成本极低，如果我们强行使用 Nest.js 等框架，恐怕要么迁移工作量爆炸，要么落地遥遥无期。</p><p><strong>二是在市场上已经有几个框架霸占了几乎是所有的市场时，我们怎么去找自己设计的框架的定位？</strong><br>在 Web 世界是 React、Vue、Angular 三大框架霸占了所有的市场，尤雨溪也曾在知乎的一个回答里（见 <a href="https://www.zhihu.com/question/332293687/answer/738922582" target="_blank" rel="noopener">https://www.zhihu.com/question/332293687/answer/738922582</a> ）说过这件事，「在三大已经几乎霸占市场的情况下怎么推一个新框架，可以看看 <a href="https://www.youtube.com/watch?v=AdNJ3fydeao" target="_blank" rel="noopener">Rich Harris 怎么推 Svelte 3 的</a>」。<br>同样的，从 GitHub Star 来看重编译类框架中 uni-app、mpvue、Taro 1/2 和重运行时类框架中的 Taro3 已经占了几乎是所有小程序框架的市场，再做一个 mpvue 或者是 Taro3 并没有意义，你无法说服用户放弃使用更知名、更稳定、生态更好的现有框架来用你的框架。<br>我感觉在这一点上，Mpx 找到一个比较好的点，和 Svelte 一样，从性能、轻量作为入手点，贴近 Vue 的语法、使用增强型设计减少学习和落地的成本，是能够吸引一部分原来是原生小程序的开发者试水 Mpx 的。</p><h3 id="包体积分析"><a href="#包体积分析" class="headerlink" title="包体积分析"></a>包体积分析</h3><p>Mpx 中谈到的小程序包体积分析工具的局限，其实在各个框架中都有，例如 Taro 等，在我们接下来的性能优化工作中，如果能有一个配套 Taro 的包体积分析工具，一定能事成功倍。补齐 Taro 生态下的包体积分析工具，将成为我们的性能优化工作中的重要一环。目前在这块我们也已经初步有产出出来了，未来再做相关的分享。</p><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p>纵然 Mpx 并不适合我们的业务场景和背景，但 Mpx 做的很多尝试还是能够引发我们的思考，启发我们的工作。</p><h1 id="抖音小程序前端渲染框架"><a href="#抖音小程序前端渲染框架" class="headerlink" title="抖音小程序前端渲染框架"></a>抖音小程序前端渲染框架</h1><p>最后一场是来自我们字节同学的分享，主要是小程序厂商内部的视角。比较有趣的是，因为讲师刚去深圳出差过，所以当时还在居家隔离中，是远程接入的。<br>另外讲师也是我的好朋友，私下也有过很多的技术交流，演讲的部分背景和内容以前也曾经聊过，所以这场听起来还是比较轻松的。</p><h2 id="小程序渲染背景介绍"><a href="#小程序渲染背景介绍" class="headerlink" title="小程序渲染背景介绍"></a>小程序渲染背景介绍</h2><h3 id="抖音小程序是什么"><a href="#抖音小程序是什么" class="headerlink" title="抖音小程序是什么?"></a>抖音小程序是什么?</h3><p>这里想必不用再介绍了，目前已经接入了不少业务，其中就包含教育，例如瓜瓜龙和清北。</p><h3 id="小程序运行时架构"><a href="#小程序运行时架构" class="headerlink" title="小程序运行时架构"></a>小程序运行时架构</h3><p>首先最底层是接入宿主 App，提供小程序运行环境和管理能力；在这之上是 Native 运行时，可能是常见的 Android 和 iOS，也可以是 IoT 设备，提供页面堆栈和端能力；再上面是跨端信道，支持多端通信；最上层就是 JSSDK，也就是小程序的 JS 运行时，提供组件、API、渲染等核心能力，这也是接下来要重点关注的内容。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/d2d54ecd-8576-4307-a67b-986f68c4acf3.png" alt=""></p><h3 id="小程序运行流程"><a href="#小程序运行流程" class="headerlink" title="小程序运行流程"></a>小程序运行流程</h3><p>用户的小程序会经过四个流程，最终被运行起来：</p><ul><li>编译：小程序代码会经过一层转义，转成 JS 引擎能够识别的代码，并进行打包</li><li>下发：在打开小程序的时候由端来下发小程序</li><li>加载：下发完成后由端来加载小程序</li><li>运行：加载完成后小程序开始运行<br>接下来关注的就是编译和运行两个流程。</li></ul><h3 id="小程序渲染背景"><a href="#小程序渲染背景" class="headerlink" title="小程序渲染背景"></a>小程序渲染背景</h3><p>小程序渲染要同时服务内部和外部开发者，内部主要是 view、text 等内置组件的渲染，外部则是开发者需要在页面上使用的自定义组件等渲染，这两者是很紧密的，但诉求又不一样，在一些方面是完全相反的。</p><p>在内部，会比较重视性能，对于高抽象程度的设计一般是难以接受的，因为往往抽象会带来更大的开销；内部需要更能接触底层和本质，开发的学习成本会较高。<br>对于外部开发者来说：</p><ul><li>对小程序语法一致性要求比较高，不能因为换了一个平台和渲染框架，就要重新学习和开发一遍</li><li>小程序的双线程渲染模型决定了线程间通信的数据必须要是可序列化的，所以类似 Proxy、Observable 等思路是行不通的</li><li>对性能有要求</li><li>对可测试性也会有要求</li></ul><p>总结来说，开发者会关注开发的限制，又关注学习成本，不接受较高的学习成本。根据上述的背景，小程序第一版的渲染框架做了两套分离的组件设计。<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/ff49af8d-b5b3-4e89-aeb2-e20c7002b281.png" alt=""></p><p>这样一套分离的方案在早期带来了一些好处：</p><ul><li>抽象不一样，可以并行去开发</li><li>分离的设计和实现更容易做单独的测试</li></ul><p>但这种分离的方案却可能带来更多的问题。如图所示，内部组件是基于 Polymer 实现的 web component，使用原生事件避免合成事件带来的开销，并借助 DOM 生命周期去做了组件的生命周期；自定义组件完全不一样，因为有高定制化的需求，所以基于 VDOM 的方案去做了组件，事件也是合成事件，并且生命周期也支持自定义生命周期。</p><p>这样分离的方案给后续的开发和维护带来了非常多的问题，比如在两端都需要做一些开发工作的时候，渲染层的开发同学必须要同时维护两套，并且要保证两边是对等的；如果开发者同时依赖了内部组件和外部组件，也会发现生命周期是不对齐的。</p><p>针对这些问题，因此需要用一套统一的组件化机制，来将两者统一起来。</p><h2 id="框架设计与思考"><a href="#框架设计与思考" class="headerlink" title="框架设计与思考"></a>框架设计与思考</h2><h3 id="为什么不用XXX？"><a href="#为什么不用XXX？" class="headerlink" title="为什么不用XXX？"></a>为什么不用XXX？</h3><p>为什么不用现有的开源方案例如 React、Vue、Svelte？因为应用场景不一样。<br>当下的诉求是：</p><ul><li><p>轻量，可插拔</p></li><li><p>独立绘制：小程序有多个平台，给多个平台提供统一的方案<br>对于这两点，React 看起来都满足要求，React 也支持 custom render，但从 16 开始 Fiber 和 Concurrent Mode 已经很复杂很重了，React 代码不是很容易搞懂。业界里支付宝用了 React，现在已经比较难维护了，并且现在在细节上和微信是不对齐的。</p></li><li><p>高度可定制性：不希望到处去打补丁</p></li><li><p>性能优化空间：和第三点相似<br>根据这四点，会发现现有的方案都很难去完全满足现有的需求，所以觉得自己去做一套框架。</p></li></ul><h3 id="框架设计迷思"><a href="#框架设计迷思" class="headerlink" title="框架设计迷思"></a>框架设计迷思</h3><p>框架的设计需要看场景和需求，这里从三点来看框架设计：</p><ul><li>能力提供：框架到底要提供什么样的能力，组件抽象？可插拔渲染 API？</li><li>渲染机制：目前主流是 JSX vs template，两者各有优缺点，但目前业界的趋势是往静态方向发展，template 能在固定的场景下提供一些信息，便于去做 AOT</li><li>状态管理：业界方案也很多<ul><li>变更监听<ul><li>defineProperty / Proxy<ul><li>defineProperty 有数组监听的缺陷</li><li>Proxy 在 C 端兼容性得不到保证，端上不一定支持</li></ul></li></ul></li><li>set-data<ul><li>例如 React 的 setState，只能走数据 diff，不知道精准的页面更新位置</li></ul></li><li>流式更新<ul><li>例如 Cyclejs，流式的更新可以做到精准的更新，但是因为 JS 原生不支持 lazy eval（惰性求值），一般都是依赖闭包实现，JS 中这种实现虽然性能好，但是内存压力大，在 IoT 场景下受限设备性能</li></ul></li><li>变更发现<ul><li>例如 Svelte，通过固定的模板语法，这种方案的问题是源代码和编译后的代码差异较大，调试难度增大</li></ul></li></ul></li></ul><h3 id="我们的选择"><a href="#我们的选择" class="headerlink" title="我们的选择"></a>我们的选择</h3><p>对于需要的特性，全部都要：渐进式编译优化 + set-data + 运行时配置。</p><p>在外部开发能力这块，页面的模板会被编译成 render 函数，并在 render 函数中做了一些插值，用来做性能优化，最终的外部组件内会调用 render 函数来做渲染。<br>在内部类似，原生组件通过 JSX 来描述，这里不用模板的原因是因为内部组件有很多对于 VDOM 的依赖，例如 Swiper 组件，Swiper 内部需要有 SwiperItem，如果用模板的话，很难对模板去做校验。</p><h2 id="渲染优化技巧"><a href="#渲染优化技巧" class="headerlink" title="渲染优化技巧"></a>渲染优化技巧</h2><h3 id="ChildFlag"><a href="#ChildFlag" class="headerlink" title="ChildFlag"></a>ChildFlag</h3><p>结构的稳定性对于性能优化很重要，如图所示的一段结构，第一部分 <code>hello</code> 很明显固定的永远是一个字符串节点，第二部分则会在 <code>boolean</code> 和 <code>VNode</code> 两种节点之间不断切换。</p><p><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/6cde70aa-a3a9-4c48-a577-d8ebb010cf26.png" alt=""></p><p>在之前 React 对于这种场景，只会在运行时不断地 typeof 来做判断，这给运行时带来了极大的压力，现在 React 会针对这种场景去给节点做 memorize，类似 V8 中 hidden class 的优化，如果传递的参数是稳定的，V8 会跳过参数的校验直接运行。<br>通过给节点加各种标识符，线上 diff 的压力减少了 70%，也就是 diff 性能提升了三倍：<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/60b45f01-4732-402e-a674-676c0cc2165b.png" alt=""></p><p>当然这种标识符让用户手写肯定不现实，但由于有编译期的存在，可以在编译期去自动加标识符。</p><h3 id="VNode-Immutability"><a href="#VNode-Immutability" class="headerlink" title="VNode Immutability"></a>VNode Immutability</h3><p>例如图中的代码，如果 children 一直不会发生变化，就给 children 标记为不可变的节点，这样在后续的 diff 中就可以跳过它，减少 diff 工作量。</p><p><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/45d3fb18-ba1f-46c7-a63e-075b4d100d7d.png" alt=""></p><p>除了这些优化外，还做了很多的优化，例如 V8 core dump，去观察性能是否有劣化。</p><h3 id="Benchmark"><a href="#Benchmark" class="headerlink" title="Benchmark"></a>Benchmark</h3><p>当然空口无凭，也有对应的 benchmark 来证明。benchmark 中抖音小程序前端渲染框架的名字叫 yaw，意为 yet another web-component。<br>yaw 目前还有一个未上线的优化版本，其中模仿 vue-next 和 Svelte 做了 PatchFlag 等优化，没有上线是因为发现这个特性会破坏 VNode 结构，导致 VNode 残缺，Vue 可以做是因为 Vue 没有很多对于 VNode 操作的依赖，这里不再展开去聊这个问题。</p><h2 id="性能工具与调优"><a href="#性能工具与调优" class="headerlink" title="性能工具与调优"></a>性能工具与调优</h2><p>一共有三种工具，第一种是客户端性能分析工具，在客户端可以打开显示性能数据，可以看到 CPU、内存等实际的占用信息；第二种是 IDE 性能 trace 工具，可以细粒度的去看性能数据；第三种是 IDE 性能评分工具，可以粗略的去看性能优化点。三种工具详细的对比如下图：<br><img src="https://cdn-eo.daraw.cn/blog/gmtc-beijing-2021-mp/1028a6a1-eb87-46a7-903e-466609fbfb7d.png" alt=""></p><h2 id="思考-3"><a href="#思考-3" class="headerlink" title="思考"></a>思考</h2><p>这次分享更多的是 inside-out，将小程序厂商内部的细节展示出来，让外部的开发者了解其设计和实现。<br>如果对各个 Web 前端框架设计与实现感兴趣的话，框架设计部分应该都比较了解了，如果不了解也没关系，核心点这块讲师讲的很不错，可以多研究一下 PPT。<br>在渲染优化技巧这块，如果比较关注 Vue3 的话，会发现 yaw 中的优化技巧不少和 Vue3 相似，重点的思路就是将固定的部分跳过，做更加精细化的更新（参考 VueConf 2019 SH 尤雨溪的分享 <a href="https://img.w3ctech.com/VueConf2019SH_Evan.pdf" target="_blank" rel="noopener">https://img.w3ctech.com/VueConf2019SH_Evan.pdf</a> ）。</p><h1 id="写在结尾"><a href="#写在结尾" class="headerlink" title="写在结尾"></a>写在结尾</h1><p>最后感谢公司，能给我这次去 GMTC 2021 的机会，这里将我的所见所想总结成文，希望大家也能和我一样有所收获，也欢迎大家和我做进一步的交流和分享，谢谢大家。</p>]]></content>
    
    <summary type="html">
    
      &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;七月初有幸参加了 GMTC 北京 2021 小程序开发实践专场，一共听了四个演讲主题，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;京喜跨端小程序开发实践&lt;/li&gt;
&lt;li&gt;智能生成云端一体代码，提升小程序开发效率&lt;/li&gt;
&lt;li&gt;滴滴出行基于 Mpx 的复杂小程序解决方案&lt;/li&gt;
&lt;li&gt;砥砺前行—抖音小程序前端渲染框架演进&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在密集的听完一下午的分享后，感觉信息量还是比较大的，引发的思考更是不少，下面就慢慢道来。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="React" scheme="https://blog.daraw.cn/tags/React/"/>
    
      <category term="Taro" scheme="https://blog.daraw.cn/tags/Taro/"/>
    
      <category term="GMTC" scheme="https://blog.daraw.cn/tags/GMTC/"/>
    
      <category term="小程序" scheme="https://blog.daraw.cn/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
    
  </entry>
  
  <entry>
    <title>从一次 Bug 定位来看 Taro H5 的组件实现</title>
    <link href="https://blog.daraw.cn/2021/04/21/taro-h5-components/"/>
    <id>https://blog.daraw.cn/2021/04/21/taro-h5-components/</id>
    <published>2021-04-21T18:28:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>目前我们的一款 C 端产品，在初期形态为小程序，但最近受限于微信审核较慢，我们将其改为了 H5 形态，规避微信审核，同时更好的支持迭代发版。</p><p>在设计技术方案的时候，考虑到部分页面（尤其是报告类、分享类页面）之后可能会需要 H5 版本，所以选择了 Taro 作为跨端框架。</p><a id="more"></a><p>Taro 目前在维护的有 2 和 3 两个大版本，之前在另一个项目中使用过了 Taro2，发现在 Taro2 中不少 React JSX 语法是受限的，而且一旦有比较多的动态 JSX 逻辑就容易遇到 Bug；在新项目启动时，看了 Taro3 的文档，Taro3 整体做了重构，运行时的设计保证了 React 语法可以随意使用，不再受限。虽然性能会有一定折损，但大幅提升了开发体验，所以综合考虑后选择了 Taro3。</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>虽然整体来看很美好，但用深了之后还是遇到了几个 Taro H5 的 Bug，今天要分享的就是其中有一个印象深刻的 Bug：</p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715767_6c25883657c46afaacf425bc44ea41ff.png" alt=""></p><p>现象是小程序中正常：</p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715727_a16ea94b877a5468b90fb47caa7ac721.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715848_12880f0525695a5cfca00314b0e0937d.png" alt=""></p><p>而 H5 中 Modal 一直是 display: none ：</p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715763_22ce8515da1ed531a7b81c1ecff52659.png" alt=""><br><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715732_180c2b41b1e1a3dc9239066babcecd64.png" alt=""></p><p>根据现象得到了<strong>初步结论</strong>：这是 Taro 适配 H5 的 Bug 而不是 Taro 上层通用的运行时或者 React 的 Bug。</p><h2 id="排查"><a href="#排查" class="headerlink" title="排查"></a>排查</h2><h3 id="Step-1"><a href="#Step-1" class="headerlink" title="Step 1"></a>Step 1</h3><p>涉及到三方库的调试，断点+阅读源码是比较有效的排查手段。所以一边打断点，侧重看 Taro H5 侧的逻辑，一边配合略读 H5 组件库的源码来了解代码结构，这边略过断点过程，直接总结看到的结论：</p><ol><li>发现 Taro H5 的组件类似 React，但又不是，在文件顶部还引入一个叫做 stencil 的库</li></ol><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000716048_c244a6455f5ef5a87bc33a51ce88b3c3.png" alt=""></p><ol start="2"><li>还有一个 reactify-wc，看注释是修改的一个开源库，这个开源库的 Readme 表示这是一个衔接 WebComponent 和 React 的库，在 React 中能够使用 WebComponent</li></ol><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715857_7505573a44167d8eb4fe18fe38f1b537.png" alt=""></p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715778_28c1a69c69005ff7b1a57ce134d06a3f.png" alt=""></p><p>到这里可以得到进一步的结论：Taro H5 React 运行时中，借助 stencil 和 reactify-wc 实现了基于 WebComponent 的组件库，既然小程序没 Bug、React 本身也没 Bug，那么 Bug 大概率就是来源于这边，接下来重点关注这里。</p><h3 id="Step-2"><a href="#Step-2" class="headerlink" title="Step 2"></a>Step 2</h3><p>由于是组件更新时才会触发的 Bug，继续打断点看组件更新的过程，这里略过打断点的过程，只讲思路：</p><p>组件更新即 props 更新，所以侧重看 props 更新的逻辑，发现其只对新的 props 做了处理，虽然引入了 prevProps，但除了用来处理 class name 之外没做其他的事（为了保留 stencil 打标记用的 class name）：</p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715939_4856a4cbbeea8bd62326bd23b8bde00f.png" alt=""></p><p>显然这样处理明显是不能覆盖我们的场景（旧 props 中有 style，新 props 中没有），再仔细阅读以下针对 style 的处理，还可以发现有几种场景无法正常工作</p><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715881_c707ef370403bf9172e177ab2ce2eb9a.png" alt=""></p><ol><li><p>旧 props 中 style 是 string 形式，新 props 中变成了 object 形式（之前的样式不会清除，直接 patch 新的）</p></li><li><p>旧 props 中 style object 和新 props 中的 object 的 keys 不对等（只 patch 新的）</p><ol><li>从这点延展开来看，会发现动态 props 也有问题，例如可以传入 abcd 四个 key，旧 props 中存在 abc，新 props 中存在 bcd，那么应该隐式的认为 a 在新 props 中为 undefined，但现在的逻辑不会处理 a</li></ol></li></ol><h3 id="Step-3"><a href="#Step-3" class="headerlink" title="Step 3"></a>Step 3</h3><p>那么我们如何修复呢？</p><ol><li>短期方案：减少 JSX 的动态性，避免动态的 style 结构和 props 结构</li></ol><p><img src="https://cdn-eo.daraw.cn/blog/taro-h5-components/1619000715975_1bfa30934d12479f9c3317dd35586e97.png" alt=""></p><ol start="2"><li>长期方案：修复 Taro H5 React 组件的 Bug</li></ol><p>第一反应是参考下 React 怎么做的，但打开 React 的 Codebase 后就放弃了，代码量太大，没法阅读，且内部有大量的针对细小问题的处理逻辑，参考价值不大。</p><p>既然 React 无法参考，那我们可以看看其他的类 React 框架的实现，例如 Preact，整体代码量也没多少，全局搜索 props 就能很快定位到代码逻辑（<a href="https://github.com/preactjs/preact/blob/ec88035b34ad5d7843c329bcf14bbdaa77c52cf3/src/diff/props.js）：" target="_blank" rel="noopener">https://github.com/preactjs/preact/blob/ec88035b34ad5d7843c329bcf14bbdaa77c52cf3/src/diff/props.js）：</a></p><ol><li><p>先遍历旧 props，找出旧 props 有但新 props 没有的部分 key，当做新值为空值来处理</p></li><li><p>再遍历新的 props，其中 style 需要特殊处理</p><ol><li>新值是 string 直接赋值 cssText 即可</li><li>旧值是 string 先给 cssText 赋值空字符串，来清空旧值的影响</li><li>都是 object 再对新值和老值做类似 props 的 diff 和 patch</li></ol></li></ol><p>参考 Preact 的实现，我给 Taro 提交了 fix PR <a href="https://github.com/NervJS/taro/pull/9088" target="_blank" rel="noopener">https://github.com/NervJS/taro/pull/9088</a> 。</p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>Taro3 为什么要绕一圈（Stencil =&gt; WebComponent =&gt; reactify-wc =&gt; React），而不直接基于原生的 HTML 标签简单封装一下 React 组件，这样既简单也不容易出问题？</p><p>带着这个问题看了下 Taro2，发现 Taro2 确实是这么做的，说明这个思路是 work 的（<a href="https://github.com/NervJS/taro/blob/master/packages/taro-components/src/components/view/index.js）。" target="_blank" rel="noopener">https://github.com/NervJS/taro/blob/master/packages/taro-components/src/components/view/index.js）。</a></p><p>接着阅读了 Taro3 的两个官方分享（参考资料 2 和 3），知道 Taro3 做了大重构，DSL 层面同时支持了 Vue / React，为了减少适配量，Taro H5 使用了 WebComponent 将公共逻辑做了复用，减少接入 Vue / React 上层视图框架的成本；再反观 Taro2，只需要支持 React 语法，不需要考虑 Vue，所以直接简单基于原生的 HTML 标签封装成 React 组件就可以了。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://taro-docs.jd.com/taro/docs/README/" target="_blank" rel="noopener">https://taro-docs.jd.com/taro/docs/README/</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzU3NDkzMTI3MA==&amp;mid=2247483770&amp;idx=1&amp;sn=ba2cdea5256e1c4e7bb513aa4c837834" target="_blank" rel="noopener">https://mp.weixin.qq.com/s?__biz=MzU3NDkzMTI3MA==&amp;mid=2247483770&amp;idx=1&amp;sn=ba2cdea5256e1c4e7bb513aa4c837834</a></li><li><a href="https://www.yuque.com/zaotalk/posts/cz8knq#HUlMM" target="_blank" rel="noopener">https://www.yuque.com/zaotalk/posts/cz8knq#HUlMM</a></li></ol>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;目前我们的一款 C 端产品，在初期形态为小程序，但最近受限于微信审核较慢，我们将其改为了 H5 形态，规避微信审核，同时更好的支持迭代发版。&lt;/p&gt;
&lt;p&gt;在设计技术方案的时候，考虑到部分页面（尤其是报告类、分享类页面）之后可能会需要 H5 版本，所以选择了 Taro 作为跨端框架。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="React" scheme="https://blog.daraw.cn/tags/React/"/>
    
      <category term="Taro" scheme="https://blog.daraw.cn/tags/Taro/"/>
    
  </entry>
  
  <entry>
    <title>浅谈 B 站新 BV 号的设计</title>
    <link href="https://blog.daraw.cn/2020/03/25/bv-av-idgen/"/>
    <id>https://blog.daraw.cn/2020/03/25/bv-av-idgen/</id>
    <published>2020-03-25T18:00:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>最近 B 站将 AV 号切换到了 BV 号，在知乎引起了较广泛的讨论，本来我只是吃瓜群众，但是在知乎上看到了一个说用 MD5 当地址的回答，我评论 MD5 不可行后，评论区有不少人质疑我，因此写了一个回答来解释为什么 MD5 不可行，以及我对 BV 号的看法。感觉这其实是一个很有意思的问题，所以将我的回答搬到了这里。</p><a id="more"></a><blockquote><p>以下是我的回答原文：</p></blockquote><p>反对 @V6.47 说的用 MD5 作为地址的回答，我理解 @V6.47 写这个回答多半出于调侃，但是评论区还真有不少人觉得这个方案是可行的。</p><h2 id="问题的本质"><a href="#问题的本质" class="headerlink" title="问题的本质"></a>问题的本质</h2><p>回到这个问题本身，AV / BV 号本质是个 ID，即「如何设计 AV / BV 号」的本质是「如何设计视频 ID 生成器」，这个问题和「<a href="https://www.zhihu.com/question/29270034" target="_blank" rel="noopener">短 URL 系统是怎么设计的？</a>」非常相似。</p><h2 id="哈希为何不可行"><a href="#哈希为何不可行" class="headerlink" title="哈希为何不可行"></a>哈希为何不可行</h2><p>哈希算法是摘要算法，无限的信息量（视频信息）转换为有限的信息量（定宽字符串），碰撞是必然会发生的。只要有碰撞的可能，那这个方案就是绝对不可行的。</p><p>有人补充说我检测一下是不是碰撞了，碰撞给它加 1 / 加 2 / 加3 … 行不行？理论是可行的，但是这样带来了一个成本，每次发号的时候还要去库里查一下这个号发过了没。如果数据量小还好，但 B 站在切 BV 号之前似乎 AV 号就已经发到了 9000W+，每次发号前加锁去 9000W+ 中搜一遍甚至更多遍，发完号再释放锁，这个 ID 发号器的性能绝对是不合格的。况且从存储与搜索的角度来说，一般的存储是基于 B Tree 或类似数据结构的，不能用哈希值作为索引键是基本常识，哈希的随机性会导致写入性能非常差，这也是为什么一般 ID 生成器生成出来的 ID 即使不是严格递增也是趋势递增的。</p><p>从安全的角度来讲，MD5 很多年前就已经是不安全的哈希算法了，用 SHA-1 也比 MD5 强，虽然 SHA-1 也已经不安全了，但好歹 SHA-1 目前公开的也只有 Google 在 17 年发布的碰撞案例，而 MD5 的碰撞成本几乎已经没有门槛了。</p><p>同样的道理，如果我们要做文件相同性校验，可能是防搬运，也可能是做网盘的秒传功能，用 MD5 在数据量不是大到极端的场景下是没问题的，但发现 MD5 相同时，是一定要再用一些手段去防止 MD5 冲突的：文件元信息比对、换一个哈希算法再算一下等等。</p><h2 id="正确的做法"><a href="#正确的做法" class="headerlink" title="正确的做法"></a>正确的做法</h2><p>B 站的做法其实就是正确的，B 站这么多年才不到一亿个号，单机发号性能都足够用了，在连续递增的时候使用数据库自增 ID 或者自己实现一套自增 ID 就可以完美的满足需求。如果要求不连续，取消连续递增的逻辑即可，中间可以跳过一部分号不用，或者引入类似 Snowflake 的算法。</p><p>更多的可以参考短 URL 的那个问题，基本是相通的。</p><h2 id="回到问题本身"><a href="#回到问题本身" class="headerlink" title="回到问题本身"></a>回到问题本身</h2><p>单从技术角度，其实 B 站的说辞是站不住脚的：</p><p><strong>容纳更多投稿</strong><br>显然 AV 号作为数字，就算视频数量暴增，也是完全够用的，加一位就可以再获得当前容量的十倍。</p><p><strong>保护稿件信息安全</strong><br>大家的解读不外乎两个点：连续 ID 更容易被爬取，通过 ID 就可以看出来 B 站每天新增的视频数和总视频数。</p><p>目前新的 BV 转出来的 AV 已经不是连续的了，即 AV 号的生成已经换了新的方案，<strong>那么 BV 号的出现实则解决了不存在的问题</strong>：</p><p>老的连续的 AV 号是继续保留的，且提供了 AV 到 BV 的转换，那想爬老的依然可以爬；<br>新的不再是连续的 AV 号生成方案也已经解决了这两个问题，完全没有搞出 BV 号的必要，只需要告诉大家新的 AV 号不再是连续的就 OK 了。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p><strong>由上可以推断，B 站对 BV 号的解释其实是站不住脚的，BV 号的出现，更多的可能是出于产品层面的考虑。</strong></p><h2 id="参考阅读"><a href="#参考阅读" class="headerlink" title="参考阅读"></a>参考阅读</h2><ul><li><a href="https://www.zhihu.com/question/29270034" target="_blank" rel="noopener">短 URL 系统是怎么设计的？</a></li><li><a href="https://link.zhihu.com/?target=https%3A//tech.meituan.com/2017/04/21/mt-leaf.html">Leaf——美团点评分布式ID生成系统</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近 B 站将 AV 号切换到了 BV 号，在知乎引起了较广泛的讨论，本来我只是吃瓜群众，但是在知乎上看到了一个说用 MD5 当地址的回答，我评论 MD5 不可行后，评论区有不少人质疑我，因此写了一个回答来解释为什么 MD5 不可行，以及我对 BV 号的看法。感觉这其实是一个很有意思的问题，所以将我的回答搬到了这里。&lt;/p&gt;
    
    </summary>
    
    
      <category term="系统设计" scheme="https://blog.daraw.cn/categories/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    
    
      <category term="ID生成器" scheme="https://blog.daraw.cn/tags/ID%E7%94%9F%E6%88%90%E5%99%A8/"/>
    
      <category term="哈希" scheme="https://blog.daraw.cn/tags/%E5%93%88%E5%B8%8C/"/>
    
  </entry>
  
  <entry>
    <title>复盘：Vue.js 是如何成功的</title>
    <link href="https://blog.daraw.cn/2020/02/27/vuejs-review/"/>
    <id>https://blog.daraw.cn/2020/02/27/vuejs-review/</id>
    <published>2020-02-27T00:10:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>前几天 Vue.js 发布了纪录片，恰巧我从 2015 年就在生产环境使用过 Vue 1.0，看着 Vue 从 1.0 到 3.0、从前端框架混战到三足鼎立，虽然后来在生产环境中使用 Vue 并不多，但也一直保持着对 Vue 和 Evan 的关注。所以这次我想从纪录片内容入手，谈谈 Vue 是如何成功的。  </p><div class="video-container"><iframe src="https://www.youtube.com/embed/OrxmtDw4pVI" frameborder="0" loading="lazy" allowfullscreen></iframe></div><a id="more"></a><h2 id="Vue-作者尤雨溪的四个阶段"><a href="#Vue-作者尤雨溪的四个阶段" class="headerlink" title="Vue 作者尤雨溪的四个阶段"></a>Vue 作者尤雨溪的四个阶段</h2><p>Vue.js 的作者尤雨溪讲了自己作为开发者的四个阶段：学生阶段、就职 Google 阶段、就职 Meteor 阶段和全职 Vue 阶段。这与他的 LinkedIn <a href="https://www.linkedin.com/in/evanyou/" target="_blank" rel="noopener">个人主页</a>也是对应的：</p><p><img src="../img/evan-you-linkedin.png" alt=""></p><h3 id="学生阶段"><a href="#学生阶段" class="headerlink" title="学生阶段"></a>学生阶段</h3><p>尤雨溪本科专业是艺术史，研究生是艺术与科技，这时候才真正接触计算机。在学生阶段关注 Web 新技术并使用新技术实现了复杂交互，因此受到了 Google 招聘的关注，一毕业就加入了 Google Creative Lab。</p><h3 id="就职-Google-阶段"><a href="#就职-Google-阶段" class="headerlink" title="就职 Google 阶段"></a>就职 Google 阶段</h3><p>因为工作需要，开发一些不同于寻常的 Web 应用，于是尤雨溪尝试了框架 Backbone 和 Angular，接着想尝试去自己做一个框架，能实现 DOM 与 JS 对象的映射关系，来满足自己所遇到的场景。<br>尤雨溪从 2013 年六月开始开发这个框架，最初叫 Seed.js ，但是 Seed.js 已经在 npm 上被占用了；由于这是一个 View 层的库，但是直接叫 View 似乎太直白了，通过 Google 翻译发现了 view 的法语翻译 vue，只有三个字母，看上去很酷，在 npm 没有被占用，于是就有了 Vue 这个名字。<br>Vue 最初作为一个个人项目发布，此时 Vue 大约有三位数的用户（由 GitHub Star 数量推测来），这也是一批早期用户。<br>这时候尤雨溪还没想过这个可以挣钱，只是纯粹的兴趣。同时尤雨溪觉得有做框架的经验可能会对之后找工作有帮助，所以加入了 Meteor，开始了就职 Meteor 的阶段。</p><h3 id="就职-Meteor-阶段"><a href="#就职-Meteor-阶段" class="headerlink" title="就职 Meteor 阶段"></a>就职 Meteor 阶段</h3><p>尤雨溪依然在利用自己的个人时间开发 Vue，直到某一天，知名 PHP 框架作者 Laravel 作者发推说学 React 太难了，学 Vue 更简单，公开表示 Vue 很不错，因此吸引了一批 Laravel 开发者来使用 Vue。<br>在这之后，在前端社区内有着不少讨论要不要使用 Vue 的问题，为了证明 Vue 和证明自己，尤雨溪花了 2015 年所有的假期，完善了框架和文档，在十月份发布了 1.0 正式版。<br>1.0 发布后尤雨溪开始考虑全职开源工作，这面临着 Meteor VS Vue 的选择：</p><ul><li>Meteor 只是个开发者，Vue 可以从更高层的角度去做设计；</li><li>同时 Meteor 使用者越来越少，而 Vue 越来越多；<br>于是尤雨溪这时候下定决心开始全职开发 Vue 的工作。</li></ul><h3 id="Vue-js-全职阶段"><a href="#Vue-js-全职阶段" class="headerlink" title="Vue.js 全职阶段"></a>Vue.js 全职阶段</h3><p>最初的问题是思考如何养活自己，尤雨溪先开了一个 Patreon 账户，每个月大约能收到近 2000 刀；同时一家叫 Strikingly 的公司，有着一个小的开源基金，在 Strikingly 的支持下，尤雨溪得以全身心的投入 Vue 的开发。</p><h2 id="纪录片带来的思考"><a href="#纪录片带来的思考" class="headerlink" title="纪录片带来的思考"></a>纪录片带来的思考</h2><h3 id="框架不只是代码"><a href="#框架不只是代码" class="headerlink" title="框架不只是代码"></a>框架不只是代码</h3><p>纪录片中 Vue Core Team 成员 <a href="https://github.com/LinusBorg" target="_blank" rel="noopener">LinusBorg (Thorsten Lünborg)</a> 讲述了他加入 Vue Core Team 的故事：<br>早期 Vue 1.0 刚发布时，Core Team 实际上还不存在，作者都在忙于解决 Issue、更新文档等等，Vue 论坛还是一片荒地，而 LinusBorg 在 Vue 社区非常活跃，熟悉各种常见的使用问题，热情的帮助大家；在三四个月后 Evan 发送了 Slack 邀请，但是这个时候实际上他还没有提过 Issue 或者 Commit。<br>这其实背后是一个很重要的事实：框架不只是代码，文档、社区、工具链非常重要，甚至某种程度上比代码更重要。这也是 LinusBorg 在没有做出任何代码层面的贡献但是被邀请加入 Core Team 的原因：他是一位社区领袖。</p><h3 id="框架设计是-Trade-Off-的结果"><a href="#框架设计是-Trade-Off-的结果" class="headerlink" title="框架设计是 Trade Off 的结果"></a>框架设计是 Trade Off 的结果</h3><p>YouTube Vue.js 教程作者 Scott 提到了在 Vue 出现的时候前端框架的选择难题：</p><blockquote><p>Angular 2 与 1 之间存在非常大的差异，这导致 Angular 流失了大量的用户；React 带来了很多新颖的东西，但是上手有一定成本，无法像 Angular.js 那样打开一个新的 HTML 文件引入一个 CDN JS 文件就可以直接使用。  </p></blockquote><p>这时候 Vue 填补了这部分空缺：既想要上手容易、又想要强大和灵活性，Vue 在这其中是平衡性做的最好的框架，这给 Vue 带来了一些用户。</p><p>尤雨溪在 JSConf Asia 2019 也做过一个关于框架设计平衡性的演讲，在这方面有比较深入的分享，这里建议可以看一看（见参考资料「Seeking the Balance in Framework Design」）。</p><h3 id="文档的重要性"><a href="#文档的重要性" class="headerlink" title="文档的重要性"></a>文档的重要性</h3><p>有很大部分的中国开发者在学习框架时更期望有中文文档，而一些技术名字很难翻译到位。在这一点上 Vue 的中文文档很友好，因为尤雨溪是一个中国人，同时编写中英文文档，能够灵活地遣词造句。  </p><p>其实尤雨溪在纪录片中没有特地提到的是，尤雨溪的英文也非常的好（当然纪录片是英文的，这也不用特地去说明了），这给 Vue 在国外的推广带来了极大的优势。尤雨溪曾在知乎分享过他学习英语的经验（见参考资料「请各位知友分享下自己的英语学习经验吧？」），其实国内优秀的库、框架很多，但有不少限制于作者的英语能力，没有提供相关的英文文档，无形中失去了很多的外国用户；或是限制于英语口语能力，无法在国外的技术会议上进行推广，触达更多的开发者。</p><h3 id="版本升级-API-的兼容性"><a href="#版本升级-API-的兼容性" class="headerlink" title="版本升级 API 的兼容性"></a>版本升级 API 的兼容性</h3><p>纪录片中一带而过的是 Vue Conf 中提到的关于兼容性处理的方案。不得不提 Vue 在 API 设计上一直做得很稳，例如 Vue 2 发布时 API 与 Vue 1 的差异很小，Vue 3 抛开 Composition API ，与 Vue 2 差异也不大。同时 Vue 2 也有 Composition API 的插件可以使用。</p><p>反观 Angular 从 1 到 2 完全是两个框架，或多或少流失了原有的用户，并给正在使用的用户带来了升级版本成本的心智负担。</p><h3 id="Vue-体积与性能优势"><a href="#Vue-体积与性能优势" class="headerlink" title="Vue 体积与性能优势"></a>Vue 体积与性能优势</h3><p>在国内为代表的国家地区网络环境情况复杂，用户量越大越复杂的 2C 产品越重视这点。而 Vue 有着相对 Angular 和 React 来说更小的体积和更好的性能，且这个优势到了 Vue 3 有了更近一步的提升，Vue 3 一定程度上参考了 Svelte 的设计，通过更多的编译期优化减小 runtime 体积，更多可以参考尤雨溪在知乎上关于 Svelte 的回答和关于 Vue 3 的分享。</p><h3 id="技术社区的影响力输出"><a href="#技术社区的影响力输出" class="headerlink" title="技术社区的影响力输出"></a>技术社区的影响力输出</h3><p>纪录片中 CSS 专家 Sarah 讲了她和 Vue 的故事：一开始写了一篇 Vue 相关的文章，接着写成了一个系列，接着就一直写了下去；同时她贡献了 Vue VSCode Snippets 插件，并加入了 Vue Core Team。在 Vue 流行之前，Sarah 就已经是社区知名的 CSS 专家，有着很大的影响力，她写的关于 Vue 的技术文章自然会给 Vue 引来更多的用户。</p><p>在国内，尤雨溪也曾在阿里内部做过 Vue 的分享，与阿里内部的前端一起推进 Vue 的落地，同时百度等知名公司也在使用 Vue，这些给 Vue 带来了大厂背书。Weex 与 Vue 的合作也是一种探索，虽然现在 Weex 似乎已经没人提起了。另外尤雨溪本身在知乎也是一位技术大 V，在知乎的活跃或多或少给 Vue 在国内的推广带来了一定的帮助。</p><p>在另一方面，程序员大多有一种对开源的信仰，尤雨溪是为数不多的做出了世界级开源方案、也是世界顶尖的程序员和开源领袖。并且 Vue 不是一个商业公司驱动的，反观 React 曾经的 License 事件，这一点也给很多开发者和团队在选择框架时增加了对 Vue 的倾向性。这些都给 Vue 和尤雨溪带来了认可、尊重和声望。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>回顾 Vue 的发展历史，会发现做一个技术框架和做一个产品很相似，不是产品有很厉害的功能就能得到大量的用户，框架也不是技术厉害就能火起来：</p><table><thead><tr><th>产品</th><th>框架</th></tr></thead><tbody><tr><td>在设计产品时需要思考你的用户群体是谁，产品功能设计的平衡性会决定你的产品能不能吸引更多的用户。</td><td>在设计框架时需要思考你的用户群体是谁，框架架构和 API 设计的平衡性会决定你的框架能不能吸引更多的用户。</td></tr><tr><td>交互上手体验越友好，越能够吸引用户和提升留存。</td><td>框架文档、周边生态设施越完善，学习和上手成本越低，越能够吸引用户。</td></tr><tr><td>产品在版本升级时，应该做好用户的引导，如果出现比较大的功能断层，很容易流失用户。</td><td>框架在版本升级时，应该做好 API 的兼容性，breaking changes 会导致用户的流失。</td></tr><tr><td>再好的产品也需要投放、推广和拉新。</td><td>再好的框架也需要通过各种形式做分享和推广。</td></tr><tr><td>如果错过了赛道的某个时间点，可能再也没有做一款大众化产品的机会了。</td><td>如果错过了技术方向的某个时间点，可能再也没有做一款能吸引大量用户的框架的机会了。</td></tr></tbody></table><p>当然也许你的目标没有做一个很多人都在使用的框架那么大，而是想借助某项技术的兴起，得到心仪公司的 offer / 成为这个领域的技术专家等等，这其实也是很常见的一种现象：<strong>技术投机</strong>。如果我们将<strong>技术投机</strong>这个词作为一个中性化的词去看待，其实 Vue.js Documentary 也给我们带来了一些启示：</p><ul><li>在一项技术有兴起苗头的时候，这个时候是参与社区构建门槛最低的时机；</li><li>参与社区构建，提 PRs / Issues 不是唯一的途径，积极的解答问题、帮忙做 Code Review 等等都是贡献的方式，其实门槛很低；</li><li>学好英语。</li></ul><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.youtube.com/watch?v=OrxmtDw4pVI" target="_blank" rel="noopener">Vue.js: The Documentary</a></li><li><a href="https://www.youtube.com/watch?v=a4N1Sz_Y5Pg" target="_blank" rel="noopener">Vue作者尤雨溪为你分享：Vue 3.0 进展@VueConf CN 2018</a></li><li><a href="https://www.youtube.com/watch?v=ANtSWq-zI0s" target="_blank" rel="noopener">Evan You on Vue.js: Seeking the Balance in Framework Design | JSConf.Asia 2019</a></li><li><a href="https://www.zhihu.com/question/20118811/answer/14036822" target="_blank" rel="noopener">请各位知友分享下自己的英语学习经验吧？ - 知乎</a></li><li><a href="https://www.zhihu.com/question/53150351/answer/133912199" target="_blank" rel="noopener">如何看待 svelte 这个前端框架？ - 知乎</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前几天 Vue.js 发布了纪录片，恰巧我从 2015 年就在生产环境使用过 Vue 1.0，看着 Vue 从 1.0 到 3.0、从前端框架混战到三足鼎立，虽然后来在生产环境中使用 Vue 并不多，但也一直保持着对 Vue 和 Evan 的关注。所以这次我想从纪录片内容入手，谈谈 Vue 是如何成功的。  &lt;/p&gt;
&lt;div class=&quot;video-container&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/OrxmtDw4pVI&quot; frameborder=&quot;0&quot; loading=&quot;lazy&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="Vue.js" scheme="https://blog.daraw.cn/tags/Vue-js/"/>
    
  </entry>
  
  <entry>
    <title>你可能不知道的 Git</title>
    <link href="https://blog.daraw.cn/2019/12/21/you-dont-know-git/"/>
    <id>https://blog.daraw.cn/2019/12/21/you-dont-know-git/</id>
    <published>2019-12-21T20:56:00.000Z</published>
    <updated>2026-03-28T15:41:48.522Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这是我最近在团队内做的一个关于 Git 的分享，反响还不错，故用此文记录。</p></blockquote><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-01_1576938158142.png" alt=""></p><a id="more"></a><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-03_1576938158277.png" alt=""></p><p>首先，本文假设你已经对 Git 有了初步的认知：</p><ul><li>理解 Git 在本地的三种工作区域：Git 仓库、工作目录和暂存区域；</li><li>熟练操作 <code>pull</code> / <code>push</code> / <code>branch</code> / <code>checkout</code> / <code>commit</code> / … 常见指令；</li></ul><p>但是：</p><ul><li>希望能够进一步了解 Git 的一些基本原理；</li><li>偶尔会遇到与预期不一致的现象，掉入坑中。</li></ul><p>如果你已经熟读 Pro Git 等相关书籍、熟练使用各种底层命令，甚至有 Git 相关工具链的开发经验，那本文可能不太适合你。</p><p>接下来将分为两大主题，文件系统与命令操作：</p><ul><li>文件系统主题将带领大家去理解 Git 的存储模型，我们的文件、commit 等信息是如何存储的；</li><li>命令操作主题将针对我们最常用的两个命令 <code>merge</code> 和 <code>rebase</code> 去解释它们是如何工作的。</li></ul><h1 id="文件系统"><a href="#文件系统" class="headerlink" title="文件系统"></a>文件系统</h1><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-05_1576938158647.png" alt=""></p><h2 id="Snapshot-based-not-delta-based"><a href="#Snapshot-based-not-delta-based" class="headerlink" title="Snapshot-based, not delta-based"></a>Snapshot-based, not delta-based</h2><p>不知道大家有没有经历过不使用版本控制的项目，在大学期间我做第一个外包项目的时候，虽然已经了解了 Git 的基本使用方式，但是完全没有工程经验，不知道源代码版本控制的意义所在，没有选择使用版本控制，然后就吃了大亏。在开发初期还好，但在联调和交付阶段，细节变更频繁发生，而我应对变更区分版本的方式是复制文件夹，给文件夹命名版本号来区分版本，缓解了问题。</p><p>在和队友复盘的时候，我们深刻的意识到了版本控制的重要性，从那之后不管大小项目，都会使用 Git 来管理源代码版本。</p><p>当然我讲这个故事的目的并不是想说明版本控制的意义，而是想由此引出一个问题，如果你来实现一个版本控制工具，你会如何设计最核心的部分：存储？</p><p>有同学的第一反应可能会是和上面我的做法类似，把整个项目存一份，打上版本号；想的更深一点的同学，可能会想，我只存每次的变更，每次切换版本的时候将变更串起来，得到文件，这样是不是也行？</p><p>这其实是版本控制工具文件系统实现的两种思路：基于快照（snapshot-based）和基于差异（delta-based）。那么，已经知道答案的同学不要说出答案，大家再猜一猜，Git 是基于哪一种呢？</p><p>可能在平时提交 commit、merge request 等变更行为之前，大家都会习惯性的去看一下 diff，也会对照 diff 进行 Code Review，这很容易给人一种错觉：Git 是基于差异的，但实际 Git 是基于快照的。</p><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-06_1576938160834.png" alt=""></p><p>基于快照的优势在于，因为每个版本的文件都是直接保存的，可以更简单的实现快速切换版本，更易于实现分布式的特性（例如 patch），而带来的劣势在于存储空间会明显占用更多，一个文件即使改动一个字符也需要再单独存一份，当然 Git 也适度的做了一些优化，包括主动的和被动的（zlib 压缩、pack）；<br>相比之下，基于差异的优势则是存储空间占用极低，几乎接近极限，而劣势则是如果想要做到和基于快照一样的切换版本速度，需要设计更复杂算法和逻辑（例如借助索引将版本切换时间降到常数级别）。</p><p>这里不去过多探讨两种方式的差异，因为在工程中这是个取舍的问题。我们需要了解的是 Git 使用了基于快照的方式存储信息，这样便于理解接下来的内容。</p><h2 id="Objects"><a href="#Objects" class="headerlink" title="Objects"></a>Objects</h2><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-07_1576938160357.png" alt=""></p><p>接下来我们一起来对 Git 文件系统的具体实现探个究竟。</p><h3 id="blob-object"><a href="#blob-object" class="headerlink" title="blob object"></a>blob object</h3><p>接着刚刚如何实现一个版本控制工具的问题，现在我们已经确定了用基于快照的思路，像我那样每个版本把整个项目打包一份肯定是不太合适的，每个版本之间一般只会变更一小部分文件，那么如何尽可能的减少存储？大家应该会想到：没有发生变更的文件只存储一份，在各个版本之间只保存对这个文件的引用，使用过对象存储服务的同学应该会想到对象存储，使用 k-v 来存储文件，key 使用一个唯一值，value 则是文件。</p><p>Git 的核心部分正是这样一个简单的键值对存储，key 是存储对象加一些元数据一起做 SHA-1 校验运算得到的哈希值，value 是压缩过的数据对象。注意我这里的说法是对象而非文件，因为在 Git 的键值对存储系统中，数据对象（blob object）是对象（Objects）类型的一种，为什么只是一种呢？</p><h3 id="tree-object"><a href="#tree-object" class="headerlink" title="tree object"></a>tree object</h3><p>在上述的存储系统中，我们存储一个文件后，得到的只有一个哈希值作为 key，这只解决了存储文件的问题，并没有解决文件名存储的问题，要将文件和文件夹组合起来，我们还需要一个用树对象（tree object）来存储这种关系，所以在 Git 中，项目文件的存储形式可以简化如图中所示。</p><h3 id="commit-object"><a href="#commit-object" class="headerlink" title="commit object"></a>commit object</h3><p>现在有一些树对象，分别代表了我们整个项目不同版本的快照。然而问题依旧：若想重用这些快照，你必须记住所有的 SHA-1 哈希值。 并且，你也完全不知道是谁保存了这些快照，在什么时刻保存的，以及为什么保存这些快照。 而以上这些，正是提交对象（commit object）能为你保存的基本信息。</p><p>上述这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 <code>.git/objects</code> 目录下。</p><p>SHA-1 哈希值的长度是 40 字符，<code>.git/objects</code> 目录下可以看到又做了一层以 2 字符命名的子目录，子目录下的文件名长度都是 38 字符，前面的 2 字符加上文件名的 38 字符就是 SHA-1 哈希值。</p><p>多提一嘴，Git 这样做的意义主要在于优化查找速度，Git 可能跑在各种各样的操作系统文件系统之上，有的文件系统同目录下文件过多会导致查找缓慢（基于链表的文件查找时间复杂度为线性，查找速度随文件数量增长），哈希的均衡性相当于把文件打散到 16*16=256 个桶中，降低了线性的系数。</p><p>可能大家会想，如何验证你说的是真的呢？在验证之前，我们需要先了解一些不太常用的 Git 命令，他们将会帮助我们去做验证。</p><p>由于 Git 是一套完整的版本控制系统，而不是纯粹的面向用户，所以它还包含了一部分用于完成底层工作的命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起，可以与脚本配合完成一些工作。 这部分命令一般被称作「底层（plumbing）命令」，而那些更友好的命令则被称作「高层（porcelain）命令」。</p><p>以 Vue3 的仓库为例，<code>g</code> 是 <code>git</code> 的 alias，<code>cat-file</code> 可以打印出 SHA-1 值对应的对象信息， <code>master^{tree}</code> 语法表示 master 分支上最新的提交所指向的树对象，可以看到文件夹的类型是 tree，文件的类型是 blob：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) g cat-file -p master^&#123;tree&#125;</span><br><span class="line">040000 tree ddbd1f4f93be7551349641c6fd7b13853a5c3430        .circleci</span><br><span class="line">040000 tree b907db0cd33f5eee41fc6a9264eac855b93f4585        .github</span><br><span class="line">100644 blob 0a663edeb54caf4c87bce21c0ed3840b92c7c48b        .gitignore</span><br><span class="line">100644 blob f5a1bdcdd2daf271a87314a475c36ca723c0b499        .prettierrc</span><br><span class="line">040000 tree f9a363bc62a7b277b968a2ee39d0b65f3ca28e87        .vscode</span><br><span class="line">100644 blob 4ab470e7d5667b51c02671940a3b2a89890a27b4        README.md</span><br><span class="line">100644 blob 5d26120d66a6991086c11ce9e7ae15e573baaa73        api-extractor.json</span><br><span class="line">100644 blob 343a47e7808ab4a11e161afb02e83a1779cbaa59        jest.config.js</span><br><span class="line">100644 blob 95684f95724550d4ac35ad8473ab04fd19060d13        lerna.json</span><br><span class="line">100644 blob edf9f47d9289da363411f0c052de09a9c9806a21        package.json</span><br><span class="line">040000 tree aab9fcef4b707da87170027ffc23610fc36e8106        packages</span><br><span class="line">100644 blob 56d34528a7a37f796c28739cbc989c988e5d0862        rollup.config.js</span><br><span class="line">040000 tree 536d6046a5c3e6e3b0c3ef8aed1fbc5a6e4f25b7        scripts</span><br><span class="line">100644 blob 3784a8e29d1809a48e14f3893e431d6f80570f19        tsconfig.json</span><br><span class="line">100644 blob 9fb28cdba4257bb1b9888fb733c97a90ff606cd8        yarn.lock</span><br></pre></td></tr></table></figure><p>我们进一步的去查看 tree，会发现它是类似刚刚的树状结构，保存了文件夹中的文件关系：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) g cat-file -p aab9fcef4b707da87170027ffc23610fc36e8106</span><br><span class="line">040000 tree 0d025022b6f8360b3d80ee5819b112885f340404        compiler-core</span><br><span class="line">040000 tree a65012f14f650d41b47bcd7669a943367bdbbb8a        compiler-dom</span><br><span class="line">100644 blob d84d24ac08b34a020d790460bd38b73753c982a9        global.d.ts</span><br><span class="line">040000 tree 51a1a6f4477d767fcde58bc2420106ee40c99776        reactivity</span><br><span class="line">040000 tree 25b42154a55f594c0b45bc5eb154a480d843e9a8        runtime-core</span><br><span class="line">040000 tree a8de900be6577a766ff6bca3cec0fa6231fcb95a        runtime-dom</span><br><span class="line">040000 tree 9d881bdd4f349df7460339225e5ac40a8995c047        runtime-test</span><br><span class="line">040000 tree 368c477ccc2e47d1e407994fcf243afb556ad9ae        server-renderer</span><br><span class="line">040000 tree e0a3ebe69e1aff614077ab4bbf2bbd4b419c4f05        shared</span><br><span class="line">040000 tree 7d5be9ece62127b727bb6e08d20360a235709e64        template-explorer</span><br><span class="line">040000 tree 0de10b0cd3e2d94cb927ad0267af63e7131c2bb3        vue</span><br></pre></td></tr></table></figure><p>而查看 blob 则会直接打印出文件内容：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) g cat-file -p f5a1bdcdd2daf271a87314a475c36ca723c0b499</span><br><span class="line">semi: false</span><br><span class="line">singleQuote: true</span><br><span class="line">printWidth: 80</span><br></pre></td></tr></table></figure><p>这是文件系统读的方式，同样我们还可以借助 <code>hash-object</code> 写入 blob 对象，<code>write-tree</code> 将暂存区写入一个 tree 对象等等，通过底层命令实现 <code>commit</code>，这里不去深入讲解，上述内容已经足够我们对 Git 文件系统有一个大致的了解，如果不是做 Git 相关工具的开发没有太大深入必要。</p><h3 id="tag-object"><a href="#tag-object" class="headerlink" title="tag object"></a>tag object</h3><p>Git 中的第四种对象：标签对象（tag object）。Git <code>tag</code> 分为两种，轻量（lightweight）标签和附注（annotated）标签。轻量标签和分支相似，下面会分析轻量标签的存储形式；而附注标签和 <code>commit</code> 更相似，标签对象包含标签创建者信息、日期、注释信息和一个指针，主要的区别在于，标签对象通常指向一个提交对象，而不是一个树对象。</p><p>当然为了解决快照占用空间过大的问题，Git 设计了自动垃圾回收策略，一般 <code>.git/objects/</code> 目录下的对象是以松散的方式存放的，但当这些松散对象的个数超过 7000 时，Git 会自动进行压缩，形成 pack 文件，当 pack 文件多于 50 个时，Git 会把多个 pack 文件再压缩成为一个 pack 文件。<br>Git 也可以手动触发垃圾回收：<code>git gc</code>，还可以设置自动垃圾回收策略的参数，例如刚刚的 7000 限制是个魔术数字，可以通过 <code>gc.autoPackLimit</code> 修改。</p><h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs"></a>Refs</h2><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-08_1576938161011.png" alt=""></p><p>我们可以通过一次 <code>commit</code> 的 SHA-1 值来查看它的内容，但是记住这个哈希值显然是不现实的，应该把这个哈希值存起来，用一种更简单的方式记住它，这种方式就是「引用（refs）」。我们可以在 <code>.git/refs</code> 文件夹中看到这些存储了哈希值的引用文件，随意打开一个一个仓库的 refs 目录，<code>HEAD</code> / <code>branch</code> / <code>tag</code> / <code>remotes</code> 在这里一览无余。</p><h3 id="branch"><a href="#branch" class="headerlink" title="branch"></a>branch</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) cat .git/refs/heads/master</span><br><span class="line">4e91b1328dda38e342b9dd0794ee1483ad2a7002</span><br><span class="line">➜  vue-next git:(master) git log</span><br><span class="line">commit 4e91b1328dda38e342b9dd0794ee1483ad2a7002 (HEAD -&gt; master, upstream/master)</span><br><span class="line">Author: Evan You &lt;yyx990803@gmail.com&gt;</span><br><span class="line">Date:   Thu Dec 12 21:22:29 2019 -0500</span><br><span class="line"></span><br><span class="line">    chore: add package dependency graph</span><br></pre></td></tr></table></figure><p>可以看到分支 ref 文件存储了其对应的最新的 commit SHA-1，我们完全可以修改这个 SHA-1 来改变所指向的 commit，但是不建议这么做，使用 <code>git update-ref</code> 是更安全的做法。</p><h3 id="HEAD"><a href="#HEAD" class="headerlink" title="HEAD"></a>HEAD</h3><p>但 Git 还需要一个记录当前在哪个分支的文件，这就是 <code>HEAD</code>，正常情况下它是一个「符号引用（symbolic ref）」，是一个指向了引用的指针：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) cat .git/HEAD</span><br><span class="line">ref: refs/heads/master</span><br></pre></td></tr></table></figure><p>和 ref 文件相似，我们可以修改这个指针来改变当前所在的分支，但也有一个更安全的命令 <code>git symbolic-ref</code> 可以使用。</p><p>注意在 Git 中，如果你切到一个指定的 <code>commit</code>，也就是「detached HEAD」状态下，<code>HEAD</code> 文件的内容就变成了 <code>ref</code>，存储这个 <code>commit</code> 的 SHA-1 值：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) g checkout c36941c4987c38c5a9</span><br><span class="line">Note: checking out 'c36941c4987c38c5a9'.</span><br><span class="line"></span><br><span class="line">You are in 'detached HEAD' state. You can look around, make experimental</span><br><span class="line">changes and commit them, and you can discard any commits you make in this</span><br><span class="line">state without impacting any branches by performing another checkout.</span><br><span class="line"></span><br><span class="line">If you want to create a new branch to retain commits you create, you may</span><br><span class="line">do so (now or later) by using -b with the checkout command again. Example:</span><br><span class="line"></span><br><span class="line">  git checkout -b &lt;new-branch-name&gt;</span><br><span class="line"></span><br><span class="line">HEAD is now at c36941c fix(compiler-core): should apply text transform to &lt;template v-for&gt; children</span><br><span class="line">➜  vue-next git:(c36941c) cat .git/HEAD</span><br><span class="line">c36941c4987c38c5a92a1ae0d554dbf746177e71</span><br></pre></td></tr></table></figure><p>当然一般很少会进入这种状态，这种状态下提交的 <code>commit</code> 除非记住 SHA-1 值，否则很难找回这些 <code>commit</code> 记录，这些 <code>commit</code> 会变成 <code>dangling commit</code>，没有分支指向它们。</p><h3 id="tag"><a href="#tag" class="headerlink" title="tag"></a>tag</h3><p>上面我们提到 tag 分为两种（轻量标签和附注标签），并分析了附注标签，这里我们再来分析轻量标签：轻量标签和分支相似，<code>ref</code> 文件中存储着对应 <code>commit</code> SHA-1。</p><h3 id="remotes"><a href="#remotes" class="headerlink" title="remotes"></a>remotes</h3><p>远程引用（remote ref）与分支引用基本相似，其指向了最近一次与服务端通信时所知晓的远端分支对应的最新 <code>commit</code> SHA-1：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">➜  vue-next git:(master) cat .git/refs/remotes/upstream/master</span><br><span class="line">4e91b1328dda38e342b9dd0794ee1483ad2a7002</span><br></pre></td></tr></table></figure><p>与分支引用的差异在于，远端引用是只读的，虽然我们可以 <code>checkout</code> 到远程引用，但 <code>HEAD</code> 不会指向这个引用，而是进入 <code>detached HEAD</code> 状态，指向这个引用所对应的 <code>commit</code>，在之后所做的 <code>commit</code> 都不会去更新远端引用。</p><p>综上，一个 Git 仓库内部的引用和文件关系大致如图所示，图中的 <code>index</code> 是暂存区域，这里不去展开讲了。</p><p>到这里 Git 文件系统部分的讲解就结束了，接下来是命令操作的讲解。</p><h1 id="命令操作"><a href="#命令操作" class="headerlink" title="命令操作"></a>命令操作</h1><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-09_1576938158648.png" alt=""><br>在准备分享内容的时候，一开始我计划设计一个章节来讲 Git 工作流，但发现常见的工作流涉及到的大多是文件系统原理和一些基本操作，上面我们已经讲过文件系统原理，这里与其讲工作流，不如讲讲最常见的操作 <code>merge</code> 和 <code>rebase</code>，理解了它们之后工作流只不过是灵活的组合形式。</p><h2 id="merge"><a href="#merge" class="headerlink" title="merge"></a>merge</h2><p><code>merge</code> 是一个比较常用的命令，很多时候使用起来比较简单，但有的时候却会有一些不符合直觉的现象。</p><h3 id="Fast-Forward-Merge：快进式合并"><a href="#Fast-Forward-Merge：快进式合并" class="headerlink" title="Fast-Forward Merge：快进式合并"></a>Fast-Forward Merge：快进式合并</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-10_1576938160294.png" alt=""></p><p>当合并的两个分支满足祖孙/父子关系时，Git 默认会使用快进式合并，最常见的场景是 <code>pull</code>，<code>pull</code> 的本质是 <code>fetch</code> + <code>merge</code>，由于一般在执行 <code>pull</code> 的时候远端分支相对本地分支是快进的，Git 可以直接合并，默认情况下不会产生 <code>merge commit</code>，但也可以通过参数 <code>--no-ff</code> 来禁止快进式合并。</p><h3 id="Three-Way-Diff-Merge：三路合并"><a href="#Three-Way-Diff-Merge：三路合并" class="headerlink" title="Three-Way Diff Merge：三路合并"></a>Three-Way Diff Merge：三路合并</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-11_1576938161012.png" alt=""></p><p>上面我们讲到 Git 是基于快照的，每一个 <code>commit</code> 中都有全量的项目文件，所以在 <code>merge</code> 时，Git 不会使用那些中间的 <code>commit</code>，只去关注最新的 <code>commit</code>，但这还不够：</p><p>如上图所示，我们要把 <code>feature</code> 分支合入 <code>master</code> 分支，假如 <code>master</code> 分支只修改了文件 X，<code>feature</code> 分支只修改了文件 Y，在合并的时候两者是不应该产生冲突、应该将修改合在一起；但是如果我们使用两路合并算法，只去对比当前分支与目标分支的最新 <code>commit</code> 即 <code>B3</code> 与 <code>F3</code>，X 和 Y 都是不一致的，需要用户介入选择处理，体验会非常糟糕。</p><p>所以 Git 使用了更智能的三路合并算法，选取当前分支节点 <code>B3</code> 与目标分支节点 <code>F3</code> 的公共祖先节点 <code>B1</code> 作为基准（base），将这三个节点的文件依次进行比较：</p><ul><li>如果 <code>B3</code>、<code>F3</code> 的某个文件和 <code>B1</code> 中的相同，那么不产生冲突；</li><li>如果 <code>B3</code> 或 <code>F3</code> 只有一个和 <code>B1</code> 相比发生变化，那么该文件将会采用该变化了的版本；</li><li>如果 <code>B3</code>、<code>F3</code> 和 <code>B1</code> 相比都发生了变化，且变化不相同，那么则产生冲突，需要手动去合并；</li><li>如果 <code>B3</code>、<code>F3</code> 都发生了变化，且变化相同，那么不产生冲突，自动采用该变化的版本。</li></ul><p>也许有同学会想，为啥解冲突时，我看到的只有当前节点与目标节点的差异，看不到公共祖先节点，这看起来像是两路合并？这是因为 Git 存在这一个配置项 <code>merge.conflictStyle</code> ，且该配置项默认值为 <code>merge</code>，可以将其设置为 <code>diff3</code>，这样之后看到的解决冲突界面就会出现祖先节点的文件内容。</p><h3 id="Recursive-Three-Way-Diff-Merge：递归三路合并"><a href="#Recursive-Three-Way-Diff-Merge：递归三路合并" class="headerlink" title="Recursive Three-Way Diff Merge：递归三路合并"></a>Recursive Three-Way Diff Merge：递归三路合并</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-12_1576938160993.png" alt=""></p><p>有时候我们的场景可能没有上面那样简单，当前分支节点与目标分支节点之间可能存在多个公共祖先，先举一个有两个公共祖先的例子。</p><p>如图所示，<code>B3</code> 与 <code>F3</code> 之间存在着 <code>B2</code> 和 <code>F1</code> 两个公共祖先，这时候 Git 会先将 <code>B2</code> 与 <code>F1</code> 进行合并，产生一个虚拟的节点 <code>V</code>，将节点 <code>V</code> 作为公共祖先节点。</p><p>两个公共祖先节点的情况这样处理，超出两个的情况也就很类似了，Git 会递归进行上述的行为，直到只有一个祖先节点，这也是为什么叫「递归三路合并算法」。</p><p>Git 提供了一个底层命令 <code>git merge-base</code>，可以用它来查找两个 <code>commit</code> 的公共祖先节点：<code>git merge-base &lt;commit1&gt; &lt;commit2&gt;</code>；另外在 UNIX 上还有一个 <code>diff3</code> 的命令，可以对三个文件进行三路 diff，这里就不再去展开讲了，大家可以自己进行尝试。</p><h3 id="三路合并来带的陷阱"><a href="#三路合并来带的陷阱" class="headerlink" title="三路合并来带的陷阱"></a>三路合并来带的陷阱</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-13_1576938160839.png" alt=""></p><p>如果两个分支有同样的改动，并后续在一个分支上做了 <code>revert</code> 的操作，在将这两个分支进行合并的时候，<code>revert</code> 的行为会体现在最终合并的结果里，也就是最初的改动不会保留到合并的结果中。<br>最常见的场景是，当一个特性分支在开发过程中被误合入主分支后，在主分支上 <code>revert</code> 了这个 <code>merge commit</code>，在特性开发完成再次合入主分支时，会发现上次合入的代码都丢失了。我在使用 Git 的过程中经历过几次这样的情况，也有其他的小伙伴遇到过，甚至有因为丢失代码出了线上事故的真实案例。</p><p>通过 <code>git merge-base R F3</code> 我们会发现第二次 <code>merge</code> 的最近公共祖先节点是 <code>F2</code>，而已经不再是 <code>B1</code>，第一次 <code>merge</code> 改变了特性分支与主分支最新节点之间的最近公共祖先节点，根据三路合并算法的原理，<code>F1</code> <code>F2</code> 的代码在公共祖先节点 <code>F2</code> 以及 <code>F3</code> 上是一致的，那么在主分支上所做的 <code>revert</code> 修改将会被带入最终的 <code>merge 2</code>。</p><p>针对这种情况如何解决？复制粘贴代码的做法是强烈不建议的，复制粘贴很可能会再一次搞丢代码，我想到的安全的做法有两种：</p><ol><li>在主分支上再次 <code>revert R</code>（revert revert 套娃操作）；</li><li>通过 <code>cherry-pick</code> 将 <code>F1</code> 和 <code>F2</code> 一起 <code>pick</code> 到主分支上。</li></ol><h2 id="rebase"><a href="#rebase" class="headerlink" title="rebase"></a>rebase</h2><h3 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-14_1576938160090.png" alt=""></p><p>很多时候大家对 <code>rebase</code> 的认知是可以帮忙引入其他分支的改动并解除冲突，也会和 <code>merge</code> 进行对比，因为这种场景下往往 <code>merge</code> 也可以达到类似的效果，但实际 <code>rebase</code> 的使用场景更广泛一些。</p><p>刚刚说的常见场景是将当前的分支基于另一个 base 重放（reapply）历史的 commits，以 git 文档中的例子进行解释，当前我们有两个分支，<code>master</code> 与 <code>feature</code>，并且当前我们在 <code>feature</code> 分支上，在执行 <code>git rebase master</code> 后，将变成图中下方所示。</p><p>通常我们会使用它来将上游分支（一般是 <code>master</code>）上的特性引入到目标的分支上（当前工作的特性分支 / bugfix 分支），已保持当前的工作分支包含了想要的特性，并且与目标分支不存在冲突。</p><h3 id="rebase-的原理"><a href="#rebase-的原理" class="headerlink" title="rebase 的原理"></a>rebase 的原理</h3><p>Git 先将所有在目标分支但不在上游分支的 <code>commit</code> 保存到一个临时区域，这些 <code>commit</code> 和 <code>git log &lt;upstream&gt;..HEAD</code> 的结果是一样的；接着将目标分支的指针设为和上游分支一致，再从刚刚的临时区域将 <code>commit</code> 按照顺序一一的进行重放。<br>每一次的 <code>commit</code> 重放都是一次三路合并：重放 <code>F1</code> 时，选择 <code>F1</code> 和 <code>B2</code> 的公共祖先节点 <code>B1</code> 作为 base；重放 <code>F2</code> 时，选择 <code>F2</code> 和 <code>F1’</code> 的公共祖先节点 <code>B1</code> 作为 base；在 <code>commit</code> 的数量更多的情况下依次类推。</p><h3 id="修改当前分支的历史记录"><a href="#修改当前分支的历史记录" class="headerlink" title="修改当前分支的历史记录"></a>修改当前分支的历史记录</h3><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-15_1576938161366.png" alt=""></p><p>在 Git 中并不存在一个专用的修改历史的命令，但我们可以借助 <code>rebase</code> 来修改历史。这里 <code>rebase</code> 的上游可以不只是其他的分支，也可以是当前分支的历史记录节点，配合 <code>rebase</code> 的交互模式，可以实现重写当前分支的历史：修改 <code>commit</code>、合并 <code>commit</code>、拆分 <code>commit</code>、调整 <code>commit</code> 顺序等等。<br>实际使用也比较简单，可以看下 Git 文档，很容易上手，相关的文章也非常多，这里就不去赘述。</p><h3 id="如何判断我是否可以重写历史？"><a href="#如何判断我是否可以重写历史？" class="headerlink" title="如何判断我是否可以重写历史？"></a>如何判断我是否可以重写历史？</h3><ul><li>已经进入多人协作的公共分支，绝对禁止重写历史：<ul><li>如果远端分支禁用了 <code>push --force</code>，无法修改远端的记录，会导致自己与远端不一致；</li><li>如果 <code>push --force</code> 强制修改远端的记录，则会导致其他人的本地与远端的不一致；</li></ul></li><li>想要修改的历史记录只在本地，或者对应的分支只有自己在使用，远端允许 <code>push --force</code>，可以重写历史。</li></ul><h3 id="如何判断我是否应该重写历史？"><a href="#如何判断我是否应该重写历史？" class="headerlink" title="如何判断我是否应该重写历史？"></a>如何判断我是否应该重写历史？</h3><p>实际上社区存在两种观念：历史记录需要被尊重不应该被修改 vs 历史应该更清晰便于查阅，在实际的工程中，甚至存在着两种极端：</p><ul><li>禁止 <code>rebase</code>，远端完全禁止 <code>force push</code>，虽然无法禁止对于未提交的 commits 进行重写，但是一旦提交的 commits 就不再可以被变更了；</li><li>除了使用 GitLab 的 MR 做 Code Review 的代码合入之外，禁止手动 <code>merge</code>，如果需要引入特性提前排除冲突，必须使用 <code>rebase</code> / <code>cherry-pick</code>。</li></ul><p>这里不去探讨应该认可哪种观念，但客观来讲，正确、适度的使用 <code>rebase</code>，可以帮助我们得到更清晰的 commit log 和 branch graph，便于之后的查阅 <code>log</code> / <code>revert</code> / <code>cherry-pick</code>。</p><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-16_1576938161012.png" alt=""></p><h1 id="问答环节"><a href="#问答环节" class="headerlink" title="问答环节"></a>问答环节</h1><p><img src="https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-17_1576938158628.png" alt=""></p><h2 id="1-​最佳实践"><a href="#1-​最佳实践" class="headerlink" title="1. ​最佳实践"></a>1. ​最佳实践</h2><p>记最佳实践不如理解本质。分享我的一些个人习惯，算不上最佳实践：</p><ul><li>​借助 <code>rebase</code>，尽量保证 <code>commit</code> 的原子性，便于 <code>revert</code> / <code>cherry-pick</code></li><li>​如果 <code>feature</code> 分支只有我一个人在使用，且没有禁用 force push，则经常 <code>rebase</code> <code>master</code></li><li>​尽量不拿子分支去 <code>merge</code> 祖先分支，保证分支线清晰</li><li>除非很明确目的，否则在 <code>merge</code> 时不做 <code>squash</code></li><li>配置 <code>alias</code>，少打一些字母，减少出错率</li><li>​善用 <code>stash</code>，保存未完成的工作</li></ul><h2 id="2-​误提交的大文件，从-git-历史记录中完全移除的方法"><a href="#2-​误提交的大文件，从-git-历史记录中完全移除的方法" class="headerlink" title="2. ​误提交的大文件，从 .git 历史记录中完全移除的方法"></a>2. ​误提交的大文件，从 .git 历史记录中完全移除的方法</h2><p>commit 的链式结构保证了无法单独修改某一条而不影响后续的记录：</p><ul><li>​如果是多人合作的仓库，不建议这样操作</li><li>如果是单人使用的仓库且可以强制 <code>push</code>，可以考虑。</li></ul><p>具体做法：借助 <code>filter-branch</code> 将涉及到的 <code>commit</code> 全部重写，再执行垃圾回收，参考 Pro Git 10.7 章节 Removing Objects 部分。</p><h2 id="3-stash-没法将新增的文件一起存起来"><a href="#3-stash-没法将新增的文件一起存起来" class="headerlink" title="3. stash 没法将新增的文件一起存起来"></a>3. stash 没法将新增的文件一起存起来</h2><p><code>git stash -u</code> 即可，<code>-u</code> 表示带上未被 tracked 的文件。</p><p>还有一部分提问这里记不太清了，如果有其他问题欢迎在评论区讨论。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://git-scm.com/book/en/v2/" target="_blank" rel="noopener">Pro Git</a></li><li><a href="https://git-scm.com/docs" target="_blank" rel="noopener">Git Reference</a></li><li><a href="http://blog.plasticscm.com/2011/09/merge-recursive-strategy.html" target="_blank" rel="noopener">Merge recursive strategy</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;这是我最近在团队内做的一个关于 Git 的分享，反响还不错，故用此文记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-eo.daraw.cn/blog/you-dont-know-git/git-01_1576938158142.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
    
    </summary>
    
    
      <category term="软件工程" scheme="https://blog.daraw.cn/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="Git" scheme="https://blog.daraw.cn/tags/Git/"/>
    
      <category term="版本控制" scheme="https://blog.daraw.cn/tags/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>React 应用性能优化一例</title>
    <link href="https://blog.daraw.cn/2019/07/02/react-performence/"/>
    <id>https://blog.daraw.cn/2019/07/02/react-performence/</id>
    <published>2019-07-02T21:26:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在我们的应用中有一个存储在 Redux 中的全局状态机，保存着整个应用的核心状态，其类型大概如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> FSMStatus &#123;</span><br><span class="line">    Unknown = <span class="number">0</span>,</span><br><span class="line">    Active = <span class="number">1</span>,</span><br><span class="line">    Inactive = <span class="number">2</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> FSM &#123;</span><br><span class="line">    global_id: <span class="built_in">number</span>; <span class="comment">// 状态机实例的全局唯一 ID</span></span><br><span class="line">    timestamp: <span class="built_in">number</span>; <span class="comment">// 当前时间，每隔一秒会变化</span></span><br><span class="line">    fsm_status: FSMStatus; <span class="comment">// 整个状态机的状态</span></span><br><span class="line">    x_mod_status: FSMStatus; <span class="comment">// x 模块的状态</span></span><br><span class="line">    x_mod_data: <span class="built_in">any</span>; <span class="comment">// x 模块的核心数据</span></span><br><span class="line">    y_mod_status: FSMStatus; <span class="comment">// y 模块的状态</span></span><br><span class="line">    y_mod_data: <span class="built_in">any</span>; <span class="comment">// y 模块的核心数据</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>还有一些 React 组件，其中 <code>Main</code> 组件包含了 <code>Inner</code> 组件，<code>Inner</code> 组件中使用了 <code>rc-tooltip</code> 实现了弹窗效果。弹窗中有一个列表，列表中有一条带有特殊样式的处于激活状态的数据，且可以通过按键操作切换处于激活状态的数据项。  </p><p>某天突然用户报告说操作弹窗列表时感到卡顿，而且在弹窗开着的时候其他模块会明显感觉比较卡。</p><a id="more"></a><h2 id="解决过程"><a href="#解决过程" class="headerlink" title="解决过程"></a>解决过程</h2><h3 id="定位原因"><a href="#定位原因" class="headerlink" title="定位原因"></a>定位原因</h3><p>拉上一个大佬一起帮忙看了 Performence 的相关信息，发现大部分掉帧时伴随着大量 CPU 占用，而相关时间段内大部分 CPU 耗时在 DOM 操作上，DOM 操作的来源在于缩略图所使用的 <code>rc-tooltip</code> 组件和和其使用的 <code>dom-align</code> 库。</p><p>通过阅读 <code>rc-tooltip</code> 相关实现源码得知，每次组件重新渲染时都需要重新计算相关的位置信息，以保证浮层以正确的大小展示在正确的位置。即这一块很难有优化空间，在现有的需求下放弃使用 <code>rc-tooltip</code> 我们也不大可能能写出一个性能明显更好的库。</p><h3 id="解决问题"><a href="#解决问题" class="headerlink" title="解决问题"></a>解决问题</h3><p>直觉猜测是上层业务组件（<code>Inner</code>）使用了 <code>fsm</code> 作为 <code>props</code> 的一部分，而 <code>fsm</code> 的频繁更新导致了组件的频繁重新渲染。<br>为了验证猜测，打开 <code>rc-tooltip</code> 弹窗，但不进行任何操作，录制性能信息，发现果然伴随着时间的变化（<code>fsm</code> 的变化），会出现来自 <code>rc-tooltip</code> 的 DOM 操作，即验证了猜测。</p><p>于是开始着手优化 <code>Inner</code> 组件，从 <code>fsm</code> 中只挑出使用到的属性：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> pick <span class="keyword">from</span> <span class="string">'lodash/pick'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 和实际情况作了一定简化</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> connect(<span class="function">(<span class="params">&#123; store &#125;: &#123; store: Store &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// before</span></span><br><span class="line">    <span class="comment">// return &#123;</span></span><br><span class="line">    <span class="comment">//     fsm: store.fsm</span></span><br><span class="line">    <span class="comment">//     user_id: store.user_id</span></span><br><span class="line">    <span class="comment">// &#125;;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// after</span></span><br><span class="line">    <span class="keyword">const</span> fsm = pick(store.fsm, [<span class="string">'x_mod_status'</span>, <span class="string">'y_mod_status'</span>]);</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        fsm，</span><br><span class="line">        user_id: store.user_id</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;)(Toolbar);</span><br></pre></td></tr></table></figure><p>并将 <code>React.Component</code> 更换为 <code>React.PureComponent</code>，以获取自动化的 <code>shouldComponentUpdate</code> 检查。</p><p>但是在这一顿操作时候性能依然没有得到改善，debug 后发现实际每次 <code>props.fsm</code> 都是新构造的对象，而 <code>React.PureComponent</code> 自动化的 <code>shouldComponentUpdate</code> 检查只会做 shallow compare，导致了前一次与后一次的 <code>props</code> 不相等，<code>shouldComponentUpdate</code> 永远返回 <code>false</code>。<br>于是自己手动填充 <code>shouldComponentUpdate</code> 的逻辑，借助 <code>lodash/isEqual</code> 对 <code>props</code> 和 <code>state</code> 做 deep compare ：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> isEqual <span class="keyword">from</span> <span class="string">'lodash/isEqual'</span>;</span><br><span class="line"></span><br><span class="line">shouldComponentUpdate(prevProps: IProps, prevState: IState) &#123;</span><br><span class="line">    <span class="comment">// 每次的 fsm 都是新的对象，shallow compare 会认为变化了，需要 deep compare</span></span><br><span class="line">    <span class="keyword">return</span> !(isEqual(prevProps, <span class="keyword">this</span>.props) &amp;&amp; isEqual(prevState, <span class="keyword">this</span>.state));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再次测试，在静止状态下 <code>fsm</code> 的变化不再触发 <code>rc-tooltip</code> 的重新渲染。但是在按住按键不放快速切换激活项的情况下，依然有卡顿感。通过观察 Performence 录制的信息，发现 <code>setState</code> 的性能损耗也很可观，在其调用链上发现最终和上面一样触发了 <code>rc-tooltip</code> 的重新渲染。</p><p>通过阅读代码发现在 <code>Main</code> 组件中存储了列表中激活状态的数据，并将这个状态从 <code>Props</code> 中依次传给了 <code>Inner</code> 、<code>rc-tooltip</code> 中的列表组件 ，在按住按键不放的时候，按键事件的回调中会不断 <code>setState</code> 更新激活状态的数据 ，从而导致组件从 <code>Main</code> 开始依次往下重新渲染，在 <code>Inner</code> 这里触发了 <code>rc-tooltip</code> 的重新渲染。</p><p>直觉的思路是在组件内部存储一个私有变量，延迟并批量进行 <code>setState</code> 的调用，但想了下这样并不可行，因为列表页当前被激活的选项的 CSS 样式效果是需要实时跟着当前状态变化的。</p><p>批量更新的思路不可行，只能想办法把这个被频繁更新的状态和使用了 <code>rc-tooltip</code> 的 <code>Inner</code> 剥离开来，从而避免 <code>Inner</code> 的重新渲染和 <code>rc-tooltip</code> 的重新渲染。通过阅读代码，发现这个状态实际只有列表组件才会使用到，于是将该状态和部分逻辑剥离到最下层的列表组件中。</p><p>经过再次修改后，验证发现这一块不再有性能问题了。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><h3 id="尽可能细化使用到的对象-props-属性"><a href="#尽可能细化使用到的对象-props-属性" class="headerlink" title="尽可能细化使用到的对象 props 属性"></a>尽可能细化使用到的对象 props 属性</h3><p>不要直接将整个 <code>fsm</code> 挂在 props 上，由于它是个对象，且上面有很多无关的信息在频繁地更新，即使 deep compare 也可能会因为不相关的属性变化带来不必要的重新渲染。</p><p>因此要么借助 <code>lodash/pick</code> 将 <code>fsm</code> 使用到的属性 <code>pick</code> 出来，并在 <code>shouldComponentUpdate</code> 中借助 <code>lodash/isEqual</code> 进行 deep compare ；要么将 <code>fsm</code> 上使用到的属性直接挂到 <code>props</code> 上（但是这对 <code>xx_data</code> 这种对象依然无解），并配合 <code>React.PureComponent</code> 使用。</p><h3 id="尽可能将-state-存到真正使用到的子组件中"><a href="#尽可能将-state-存到真正使用到的子组件中" class="headerlink" title="尽可能将 state 存到真正使用到的子组件中"></a>尽可能将 state 存到真正使用到的子组件中</h3><p>将状态放在祖先组件中，通过一层层的 <code>props.xx</code> 和 <code>props.onXXChange</code> 不但开发体验糟糕，还会导致无意义且无法避免的不相关的组件重新渲染。</p><p>如果真的是很多地方使用到的状态，可以放在 <code>redux</code> 中。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;在我们的应用中有一个存储在 Redux 中的全局状态机，保存着整个应用的核心状态，其类型大概如下：&lt;/p&gt;
&lt;figure class=&quot;highlight typescript&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;enum&lt;/span&gt; FSMStatus &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    Unknown = &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    Active = &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    Inactive = &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;,&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;interface&lt;/span&gt; FSM &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    global_id: &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt;; &lt;span class=&quot;comment&quot;&gt;// 状态机实例的全局唯一 ID&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    timestamp: &lt;span class=&quot;built_in&quot;&gt;number&lt;/span&gt;; &lt;span class=&quot;comment&quot;&gt;// 当前时间，每隔一秒会变化&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    fsm_status: FSMStatus; &lt;span class=&quot;comment&quot;&gt;// 整个状态机的状态&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    x_mod_status: FSMStatus; &lt;span class=&quot;comment&quot;&gt;// x 模块的状态&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    x_mod_data: &lt;span class=&quot;built_in&quot;&gt;any&lt;/span&gt;; &lt;span class=&quot;comment&quot;&gt;// x 模块的核心数据&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    y_mod_status: FSMStatus; &lt;span class=&quot;comment&quot;&gt;// y 模块的状态&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    y_mod_data: &lt;span class=&quot;built_in&quot;&gt;any&lt;/span&gt;; &lt;span class=&quot;comment&quot;&gt;// y 模块的核心数据&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;还有一些 React 组件，其中 &lt;code&gt;Main&lt;/code&gt; 组件包含了 &lt;code&gt;Inner&lt;/code&gt; 组件，&lt;code&gt;Inner&lt;/code&gt; 组件中使用了 &lt;code&gt;rc-tooltip&lt;/code&gt; 实现了弹窗效果。弹窗中有一个列表，列表中有一条带有特殊样式的处于激活状态的数据，且可以通过按键操作切换处于激活状态的数据项。  &lt;/p&gt;
&lt;p&gt;某天突然用户报告说操作弹窗列表时感到卡顿，而且在弹窗开着的时候其他模块会明显感觉比较卡。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="React" scheme="https://blog.daraw.cn/tags/React/"/>
    
  </entry>
  
  <entry>
    <title>Thrift RPC Mock 方案探索</title>
    <link href="https://blog.daraw.cn/2019/06/12/thrift-rpc-mock/"/>
    <id>https://blog.daraw.cn/2019/06/12/thrift-rpc-mock/</id>
    <published>2019-06-12T11:53:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景分析"><a href="#背景分析" class="headerlink" title="背景分析"></a>背景分析</h2><p>在传统前后端分离开发的场景下，前端和后端一般定好 HTTP API 接口后就各自进行开发，前端开发中使用 EasyMock、webpack-api-mock 等平台/工具进行接口的 mock，后端通过 Postman / curl 等工具进行接口的自测。</p><p>在微服务场景下，各服务之间通过 IDL 定义好 RPC 接口。但是接口调用方依然有 mock 接口的需求，接口提供方也有着自测接口的需求。公司内的服务化平台已经提供了较为完善的接口测试工具，自己实现一个相对也比较容易，但目前却没有一个比较完善的 RPC Mock 方案。</p><p>在新项目启动后，前端、API 层和依赖的 Service 往往同步开始开发，只要依赖的 Service 未提供，API 和前端的开发、自测都会被阻塞，在侧重数据展示类需求的项目中这种问题更加严重。</p><p>所以，有必要尝试探索一套 RPC Mock 的方案，在保证开发者使用体验的前提下，解决上述问题。</p><a id="more"></a><h2 id="方案调研"><a href="#方案调研" class="headerlink" title="方案调研"></a>方案调研</h2><p>虽然我们的目标和常见的 HTTP API Mock 不一样，但是在设计思路上，RPC Mock 与 HTTP API Mock 其实没有本质的区别，因此我们将传统的 HTTP API Mock 方案也纳入调研范围内。</p><h3 id="Mock-js"><a href="#Mock-js" class="headerlink" title="Mock.js"></a><a href="http://mockjs.com" target="_blank" rel="noopener">Mock.js</a></h3><p>Mockjs 侧重 Mock 数据的生成，自定义了一套描述数据的 DSL，可以生成较为真实的 Mock 数据。<br>另外 Mockjs 还通过在开发本地劫持了 XHR 对象，提供了简单的 HTTP API Mock 的功能。</p><h3 id="EasyMock"><a href="#EasyMock" class="headerlink" title="EasyMock"></a><a href="https://easy-mock.com/" target="_blank" rel="noopener">EasyMock</a></h3><p>Easy Mock 属于 HTTP API Mock。<br>在数据生成方面，需要用户自己编辑接口信息以及 Response 结构体，引入了 Mockjs，使用 Mockjs 的语法描述 Response 结构体以实现生成随机数据，平台服务端还提供了 HTTP 接口服务可供调用。</p><h3 id="mocker-api"><a href="#mocker-api" class="headerlink" title="mocker-api"></a><a href="https://www.npmjs.com/package/mocker-api" target="_blank" rel="noopener">mocker-api</a></h3><p>同上属于 HTTP API Mock，前身为 <a href="https://www.npmjs.com/package/webpack-api-mocker" target="_blank" rel="noopener">webpack-api-mocker</a>。<br>在数据生成方面，用户可以从方法的维度配置固定的 Mock 数据或者配置数据生产函数，并在开发本地起了一个 HTTP Server，将本地开发的请求代理到了这个 Server 上。</p><h3 id="lushijie-thrift-mock"><a href="#lushijie-thrift-mock" class="headerlink" title="lushijie/thrift-mock"></a><a href="https://github.com/lushijie/thrift-mock/" target="_blank" rel="noopener">lushijie/thrift-mock</a></h3><p>只有 Mock Data 的部分，根据 Thrift 的字段与类型信息自动生成 Mock 数据。</p><h3 id="adispring-thrift-mock"><a href="#adispring-thrift-mock" class="headerlink" title="adispring/thrift-mock"></a><a href="https://github.com/adispring/thrift-mock" target="_blank" rel="noopener">adispring/thrift-mock</a></h3><p>只有 Mock Data 的部分，和上面的类似，可以进行二次修改生成的数据。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在 HTTP API 方面，由于协议之类的是非常通用的，所以已经有不少比较成熟的方案，而 RPC 由于各家公司的技术架构不一样，目前没有什么比较成熟的通用方案，而目前公司内也没有这方面的方案。</p><p>借鉴比较成熟的那些 HTTP API Mock 方案，基本可以确定我们的思路，方案整体分为两部分：Mock 请求 + Mock 数据。首先我们需要搭建一个能够和正常服务一样可被调用的 Mock 服务，接着在调用时返回 Mock 数据。</p><h2 id="方案设计-V1（CLI）"><a href="#方案设计-V1（CLI）" class="headerlink" title="方案设计 V1（CLI）"></a>方案设计 V1（CLI）</h2><h3 id="思路分析"><a href="#思路分析" class="headerlink" title="思路分析"></a>思路分析</h3><p>上述方案中最成熟的应该是 EasyMock，受其影响，第一反应是做成类似的样子，开发一个平台，开发者在平台上配置 Mock 数据，平台提供可被调用的 Mock RPC Server。但是由于 Thrift RPC 协议的特殊性，无法简单的在一个 IP:PORT 下部署多个服务，所以平台化提供可被调用的 Mock 服务不太可能实现。</p><p>既然无法做到类似 EasyMock 那样，那退一步和 mocker-api 那样在开发本地起一个 RPC Mock Server 肯定是没有问题的，例如把 RPC Mock Server 起在了本地的 8888 端口，业务中想要调用本地的服务而不是真实的服务时，只需要将 RPC Client 配置中的 Service Name（服务发现使用了 Consul ）改为写死的 IP:PORT 127.0.0.1:8888 即可。</p><p>于是，第一版方案的思路大致如下：<br>初版方案的设想是做成类似 mocker-api 的 CLI。CLI 想要在本地启动 RPC Server，则必须要有的三个信息：Service Name、Thrift 文件位置以及服务端口，另外还需要为用户提供可选的类似 mocker-api 的配置 Mock 数据的入口。</p><p>有了上面的基本信息后，就足以起一个 RPC Server 了，接下来是 Mock 数据生成的工作。生成 Mock 数据主要有两个思路，自动生成与用户配置。</p><ol><li>自动生成数据：已经有 Thrift IDL 文件的情况下，可以 parse thrift 文件得到数据结构字段和类型等信息，并根据类型信息使用 Mockjs 构造随机数据。</li><li>用户配置数据：Mock 数据配置文件的书写方式和 mocker-api 也很相似，提供一个 <code>commonjs</code> 文件，默认输出的对象是 key 为方法名，value 是对象或者是函数。例如我们需要配置 ID 生成器服务 <code>IDGenService</code> 的 <code>Gen</code> 方法：</li></ol><ul><li>当使用固定对象时，每次返回的都是配置的 Response。<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// IDGenService</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    Gen: &#123;</span><br><span class="line">        Id: <span class="string">"123456"</span>,</span><br><span class="line">        Extra: &#123;</span><br><span class="line">            Message: <span class="string">"success"</span>,</span><br><span class="line">            Code: <span class="number">0</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li></ul><p>如上在调用 <code>rpc.IDGenService.Gen(req)</code> 时，返回的永远是：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    Id: <span class="string">"123456"</span>,</span><br><span class="line">    Extra: &#123;</span><br><span class="line">        Message: <span class="string">"success"</span>,</span><br><span class="line">        Code: <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>有时候我们想配置一些简单的规则，以避免固定对象导致业务逻辑冲突：<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// IDGenService</span></span><br><span class="line"><span class="keyword">let</span> globalId = <span class="number">100</span>;</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    Gen: <span class="function"><span class="params">()</span> =&gt;</span> (&#123;</span><br><span class="line">        Id: <span class="string">`<span class="subst">$&#123;globalId++&#125;</span>`</span>,</span><br><span class="line">        Extra: &#123;</span><br><span class="line">            Message: <span class="string">'success'</span>,</span><br><span class="line">            Code: <span class="number">0</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></li></ul><p>如上在调用 <code>rpc.IDGenService.Gen(req)</code> 时，返回的 <code>Id</code> 每次都会递增：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    Id: <span class="string">"100"</span>, <span class="comment">// 101, 102, 103, ...</span></span><br><span class="line">    Extra: &#123;</span><br><span class="line">        Message: <span class="string">"success"</span>,</span><br><span class="line">        Code: <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>综上，用户启动 CLI 后，CLI 会在本地起一个 RPC Server，收到调用请求后，如果用户为该方法配置了 Mock 数据，则返回配置好的 Mock 数据，否则根据 Thrift 文件的类型信息动态构造出 Mock Response 内容。</p><p><img src="https://cdn-eo.daraw.cn/blog/thrift-rpc-mock/4d42f7aa78192ebab572cbe468e984b9.png" alt="CLI 处理流程"></p><h3 id="方案实现"><a href="#方案实现" class="headerlink" title="方案实现"></a>方案实现</h3><p>在确定方案后，很快开发出了基本可用的版本，由于和公司内基础设施耦合比较严重，这里不放出相关实现。</p><h3 id="方案总结"><a href="#方案总结" class="headerlink" title="方案总结"></a>方案总结</h3><p>在最近的两个项目中使用了上面的 CLI，基本能满足业务开发中的 RPC Mock 需求，但是抛开 TODO 中未实现功能点（例如 HotReload）外，发现了一些问题：</p><p><strong>不能控制方法级别的 Mock</strong><br>比如 <code>IDGenService</code> 有两个方法 <code>Gen</code> 和 <code>GenMulti</code>，我只想 Mock <code>Gen</code> 方法，不想 Mock <code>GenMulti</code> 方法，在这种方式下没有比较简单的实现方式。</p><p><strong>自动 Mock 数据基本不可用</strong><br>直接把 Thrift 中的类型信息转为 Mockjs 的配置会存在下面两个问题，从而导致数据基本不可用：</p><ol><li>可选项的处理<br>可选项存在两种情况：</li></ol><ul><li>业务意义上的选填，这种可选可不选，构造 Mock 数据的时候可以随机填写；</li><li>迭代过程中添加的字段，遵循 Thrift 的最佳实践，为了保证线上服务的正常运行，新增的字段应该选为可选项，但是在业务上是必选的，如果不选可能会报错，而我们在构造 Mock 数据的时候是不知道到底是不是该必选。</li></ul><ol start="2"><li>Mock 数据的真实性<br>很多字段是有约定或者有业务意义的，例如 <code>Code</code>、<code>Name</code>、<code>ID</code> 等等，不能直接按照 <code>int</code> <code>string</code> 等类型信息构造 mock 数据。轻则只影响展示效果，重则导致逻辑错误（例如生成一个不存在的 <code>Code</code>、负数的时长等等）。</li></ol><p><strong>CLI 形式不够友好</strong>  </p><ol><li>如果需要同时配置 N 个 Service 的 Mock，那就需要打开 N 个 Terminal Tab 并使用 CLI 起 N 个 Mock Server。</li><li>提交代码的时候得注意不能把写死的临时配置代码带上去。</li></ol><p><strong>语言无关</strong><br>当然，这个 CLI 也有一定的优点，也是 Thrift RPC 协议的优点：他是语言无关的。无论你在开发的业务使用的什么语言和框架，都可以使用。</p><p>针对上面发现的一系列问题，首先决定暂时先放弃数据的自动化生成，只支持用户自行配置数据的方式；另外和 Mentor 沟通后，他提醒我一点，既然在 Node.js 项目中，RPC Middleware 是把 RPC Client 对象挂到了 <code>ctx</code> 上（这里是 Koa，在 Express 中是挂到了 <code>req</code> 上），那么完全可以通过中间件的形式再把 <code>ctx.rpc</code> 劫持了，在 Node.js 项目中做到更简单的实现、更精细的粒度控制和更好的使用体验。</p><p>于是，有了下面的 V2 版本的方案设计。</p><h2 id="方案设计-V2（Middleware）"><a href="#方案设计-V2（Middleware）" class="headerlink" title="方案设计 V2（Middleware）"></a>方案设计 V2（Middleware）</h2><h3 id="思路分析-1"><a href="#思路分析-1" class="headerlink" title="思路分析"></a>思路分析</h3><p>由于方案 V2 中选择通过劫持 <code>ctx.rpc</code> 对象上的 <code>xService.yMethod</code> 函数实现 Mock，和 Mockjs 劫持 XHR 思路很相似，所以不再需要在本地启动 RPC Server，而且可以轻松同时配置多个 Service 的 Mock，方案 V1 中启动 RPC Server 必要的三个信息（Service Name、Thrift File、Server Port）也不再需要，开发者使用时只需要提供一个 Mock 数据配置文件即可。</p><p>在配置文件中，配置需要 Mock 的 Service 和方法，以及方法对应的 Mock 数据 / 数据生成函数（和 V1 相似），配置文件中暴露出来的对象的类型如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> Mock &#123;</span><br><span class="line">    [serviceName: <span class="built_in">string</span>]: &#123;</span><br><span class="line">        [methodName: <span class="built_in">string</span>]: <span class="built_in">any</span>; <span class="comment">// object | Function</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>相比 V1 中的配置方式，只是加了一个层级用于配置 Service，以便同时做到多 Service 支持。</p><h3 id="方案实现-1"><a href="#方案实现-1" class="headerlink" title="方案实现"></a>方案实现</h3><p>同上，和内部基础设施耦合，不放出相关具体实现。<br>使用时只需要在 rpc 中间件之后再添加 mock 中间件即可，对于业务代码的入侵极低。另外可以加一下环境的判断，只有在开发环境才会使用 mock 中间件。</p><p>要注意的是，由于需要劫持 ctx.rpc，所以必须要在 rpc 中间件之后使用 mock 中间件，否则 mock 不会生效。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> mockConfig = path.resolve(__dirname, <span class="string">'mock/mock.config.js'</span>);</span><br><span class="line"><span class="comment">// 一定要在 rpc 中间件之后引入 mock 中间件</span></span><br><span class="line">app.use(rpc(rpcConfig));</span><br><span class="line">app.use(mock(mockConfig));</span><br></pre></td></tr></table></figure><p>其中只有配置了 Mock 的 Service 的 Method 才会生效。例如 <code>IDGenService</code> 有 <code>Gen</code> 和 <code>GenMulti</code> 两个方法，如果我们配置了其他 Service 但是没有配置 <code>IDGenService</code>，那么 <code>IDGenService</code> 上的 RPC 方法被调用时会直接请求真实的服务；如果只配置了 <code>IDGenService</code> 的 <code>Gen</code> 方法，那么在调用 <code>ctx.rpc.IDGenService.Gen</code> 会命中 Mock，而 <code>ctx.rpc.IDGenService.GenMulti</code> 依然会请求真实的服务。即可以做到 Service + Method 粒度的配置。</p><p>在中间件中其实是做了两层嵌套循环：遍历 <code>ctx.rpc</code> 对象上的所有 Service，如果在配置文件中命中了该 Service，则对该 Service 对象再做一次遍历，遍历其上的所有 Method，如果在配置文件中命中了 Method，才会劫持该 Method。实现其实很简单，理解了实现也就理解了上面配置的 Service + Method 的粒度控制。</p><p>另外在实际使用中，建议在项目代码目录下添加一个 mock 文件夹，将配置文件放到该文件夹下，并将该文件夹添加到 nodemon 等监听文件修改热重载工具的监听范围内，这样在配置文件更新后也能获得热重载的能力，并且团队内可以共用 Mock 数据。</p><h3 id="方案总结-1"><a href="#方案总结-1" class="headerlink" title="方案总结"></a>方案总结</h3><p>V2 中间件的方案更多的是解决了 Node.js 项目的 Mock 实现方式，但我们还有其他方面需要优化：  </p><p><strong>Mock 数据构造</strong><br>IDL 中的字段名字、类型是有用的，利用好是可以减少 Mock 数据的构造成本的。但是这块如何做好后续依然需要探索，目前手动构造的方案虽然需要成本，但是也是可以接受的。</p><p><strong>跨语言/框架支持</strong><br>相比 V1 CLI 的形式，V2 中间件的形式目前只支持 Nodejs 的 Koa / Express 框架，但是 Mock 的需求是普遍存在的。由于 JS 动态语言的特点，所以劫持变量可以很简单的实现 Nodejs 项目的 Mock Middleware，如何做其他语言 / 框架的支持，后续依然值得探索。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;背景分析&quot;&gt;&lt;a href=&quot;#背景分析&quot; class=&quot;headerlink&quot; title=&quot;背景分析&quot;&gt;&lt;/a&gt;背景分析&lt;/h2&gt;&lt;p&gt;在传统前后端分离开发的场景下，前端和后端一般定好 HTTP API 接口后就各自进行开发，前端开发中使用 EasyMock、webpack-api-mock 等平台/工具进行接口的 mock，后端通过 Postman / curl 等工具进行接口的自测。&lt;/p&gt;
&lt;p&gt;在微服务场景下，各服务之间通过 IDL 定义好 RPC 接口。但是接口调用方依然有 mock 接口的需求，接口提供方也有着自测接口的需求。公司内的服务化平台已经提供了较为完善的接口测试工具，自己实现一个相对也比较容易，但目前却没有一个比较完善的 RPC Mock 方案。&lt;/p&gt;
&lt;p&gt;在新项目启动后，前端、API 层和依赖的 Service 往往同步开始开发，只要依赖的 Service 未提供，API 和前端的开发、自测都会被阻塞，在侧重数据展示类需求的项目中这种问题更加严重。&lt;/p&gt;
&lt;p&gt;所以，有必要尝试探索一套 RPC Mock 的方案，在保证开发者使用体验的前提下，解决上述问题。&lt;/p&gt;
    
    </summary>
    
    
      <category term="服务端" scheme="https://blog.daraw.cn/categories/%E6%9C%8D%E5%8A%A1%E7%AB%AF/"/>
    
    
      <category term="RPC" scheme="https://blog.daraw.cn/tags/RPC/"/>
    
      <category term="Thrift" scheme="https://blog.daraw.cn/tags/Thrift/"/>
    
      <category term="Mock" scheme="https://blog.daraw.cn/tags/Mock/"/>
    
      <category term="Node.js" scheme="https://blog.daraw.cn/tags/Node-js/"/>
    
  </entry>
  
  <entry>
    <title>前端视频质量监控</title>
    <link href="https://blog.daraw.cn/2018/09/07/video-quality-monitor/"/>
    <id>https://blog.daraw.cn/2018/09/07/video-quality-monitor/</id>
    <published>2018-09-07T00:10:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>业务中使用到视频播放后，一些不确定的因素例如用户端网络异常、CDN 异常等等，导致视频加载缓慢和发生卡顿，这些质量问题会给用户体验带来较大的伤害，也会影响产品的留存转化率。因此对线上视频进行质量监控意义很大，可以让我们明确知道用户端的异常发生概率，以便做进一步的优化。</p><a id="more"></a><p>在我们的业务中，是不支持用户自己控制视频播放/暂停以及播放进度的，所以我们不需要考虑一些用户控制的边界情况；另外一部分视频会在挂载到真实 DOM 前做预加载（preload）处理，因此我们要留意下这类视频和无预加载的视频的差异。<br>另外我们的业务只在 PC 上，而且用户使用的都是现代浏览器（Chrome 为主），所以不需要考虑移动端的兼容性以及老浏览器的兼容性。</p><h2 id="思路与实现"><a href="#思路与实现" class="headerlink" title="思路与实现"></a>思路与实现</h2><p><code>video</code> Element 在播放视频的过程中会触发一系列的媒体事件（Media events），通过查阅 MDN Media events 列表，可以发现值得关注的几个事件分别是 <code>canplay</code> <code>canplaythrough</code> <code>error</code> <code>loadeddata</code> <code>loadedmetadata</code> <code>loadstart</code> <code>play</code> <code>playing</code> <code>waiting</code>。  </p><p>首先 <code>error</code> 事件可以不用特别关注，和常规错误监控一样处理即可；重点在于如何统计加载耗时及卡顿现象。  </p><p>接下来我们拿一个视频简单做下试验，看下这几个事件触发的时机以及先后顺序。<br><strong>测试环境：macOS 10.13.6 / Chrome 68</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[<span class="string">'loadstart'</span>, <span class="string">'loadedmetadata'</span>, <span class="string">'loadeddata'</span>, <span class="string">'waiting'</span>, <span class="string">'canplay'</span>, <span class="string">'canplaythrough'</span>, <span class="string">'error'</span>, <span class="string">'play'</span>, <span class="string">'playing'</span>, <span class="string">'ended'</span>].forEach(<span class="function">(<span class="params">eventName</span>) =&gt;</span> &#123;</span><br><span class="line">  video.addEventListener(eventName, () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> key = <span class="string">`_<span class="subst">$&#123;eventName&#125;</span>_time`</span>;</span><br><span class="line">    video[key] = <span class="built_in">Date</span>.now();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">console</span>.log(eventName, <span class="built_in">Date</span>.now());</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>对于一个无预加载的视频来说，从开始加载到正常播放，输出日志为：</p><table><thead><tr><th>event</th><th>time</th><th>cost</th></tr></thead><tbody><tr><td>play</td><td>1536561748438</td><td>0</td></tr><tr><td>waiting</td><td>1536561748438</td><td>0</td></tr><tr><td>loadstart</td><td>1536561748455</td><td>17</td></tr><tr><td>loadedmetadata</td><td>1536561748588</td><td>133</td></tr><tr><td>loadeddata</td><td>1536561748613</td><td>25</td></tr><tr><td>canplay</td><td>1536561748621</td><td>8</td></tr><tr><td>playing</td><td>1536561748625</td><td>4</td></tr><tr><td>canplaythrough</td><td>1536561748627</td><td>2</td></tr></tbody></table><p>对于一个有预加载处理的视频来说，对应的输出日志为：</p><table><thead><tr><th>event</th><th>time</th><th>cost(ms)</th></tr></thead><tbody><tr><td>loadstart</td><td>1536562506189</td><td>0</td></tr><tr><td>loadedmetadata</td><td>1536562506271</td><td>82</td></tr><tr><td>loadeddata</td><td>1536562506304</td><td>33</td></tr><tr><td>canplay</td><td>1536562506305</td><td>1</td></tr><tr><td>canplaythrough</td><td>1536562506307</td><td>2</td></tr><tr><td>play</td><td>1536562526391</td><td>20084</td></tr><tr><td>playing</td><td>1536562526392</td><td>1</td></tr></tbody></table><p>观察上述事件触发的顺序，对应 MDN 上对事件的描述，与视频初始化相关的几个事件 <code>loadstart</code> <code>loadedmetadata</code> <code>loadeddata</code> 在视频加载的时候会依次触发，分别代表着开始加载、元信息加载成功、首帧加载成功，因此在统计视频加载延迟的时候，我们基本可以确定统计这三个事件触发的时间差即为加载延迟。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> logLatency = <span class="function">(<span class="params">video</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (video._loadstart_time &amp;&amp; video._loadedmetadata_time &amp;&amp; video._loadeddata_time) &#123;</span><br><span class="line">    <span class="keyword">const</span> loadedmetadataCost = video._loadedmetadata_time - video._loadstart_time;</span><br><span class="line">    <span class="keyword">const</span> loadeddataCost = video._loadeddata_time - video._loadstart_time;</span><br><span class="line"></span><br><span class="line">    logger.log(<span class="string">'Latency'</span>, [loadedmetadataCost, loadeddataCost]);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">[<span class="string">'loadstart'</span>, <span class="string">'loadedmetadata'</span>, <span class="string">'loadeddata'</span>].forEach(<span class="function">(<span class="params">eventName</span>) =&gt;</span> &#123;</span><br><span class="line">  video.addEventListener(eventName, <span class="function"><span class="keyword">function</span> <span class="title">callback</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> key = <span class="string">`_<span class="subst">$&#123;eventName&#125;</span>_time`</span>;</span><br><span class="line">    video[key] = <span class="built_in">Date</span>.now();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在 loadeddata 时记录延迟</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'loadeddata'</span>) &#123;</span><br><span class="line">      logLatency(video);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 延迟只需记录一次，相关的监听器触发过后则可移除</span></span><br><span class="line">    video.removeEventListener(eventName, callback);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>接下来我们在播放过程中用 Chrome Dev Tool 的 Network 模拟弱网（网络抖动），输出日志为：</p><table><thead><tr><th>event</th><th>time</th><th>cost(ms)</th></tr></thead><tbody><tr><td>waiting</td><td>1536565784729</td><td>0</td></tr><tr><td>canplay</td><td>1536565785075</td><td>346</td></tr><tr><td>playing</td><td>1536565785076</td><td>1</td></tr></tbody></table><p>观察上述事件触发的顺序，对应 MDN 上对事件的描述，与视频初始化相关的几个事件 <code>waiting</code> <code>canplay</code> <code>playing</code> 在视频播放过程中发生卡顿到再次播放的时候会依次触发，分别代表着等待、加载了足够的数据可以播放、<code>play</code> 事件后有足够多的数据可以开始播放或者从卡顿缓冲中恢复过来。在统计视频卡顿的时候，我们基本从这三个事件入手。  </p><p>对比初次加载时的事件触发顺序，可以不用关注 <code>canplay</code> 事件，只关注 <code>waiting</code> 和 <code>playing</code>，但是这里又有一个问题就是无预加载的视频在刚开始播放时会触发 <code>waiting</code> 和 <code>playing</code>，预加载的视频在刚开始播放时只会触发 <code>playing</code>，所以我们要忽略掉第一次 <code>playing</code> 事件，从第二次开始记录 <code>waiting</code> 和 <code>playing</code> 的时间差，即为卡顿时长（卡顿缓冲耗时）。  </p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> logBuffer = <span class="function">(<span class="params">video</span>) =&gt;</span> &#123;</span><br><span class="line">  video._buffer_times += <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 初次缓冲为正常缓冲，不上报</span></span><br><span class="line">  <span class="keyword">if</span> (video._buffer_times === <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (video._waiting_time &amp;&amp; video._playing_time) &#123;</span><br><span class="line">    <span class="keyword">const</span> bufferCost = video._playing_time - video._waiting_time;</span><br><span class="line">    logger.log(<span class="string">'Buffer'</span>, [bufferCost]);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">video._buffer_times = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">[<span class="string">'waiting'</span>, <span class="string">'playing'</span>, <span class="string">'ended'</span>].forEach(<span class="function">(<span class="params">eventName</span>) =&gt;</span> &#123;</span><br><span class="line">  video.addEventListener(eventName, () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> key = <span class="string">`_<span class="subst">$&#123;eventName&#125;</span>_time`</span>;</span><br><span class="line">    video[key] = <span class="built_in">Date</span>.now();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在 playing 时记录卡顿</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'playing'</span>) &#123;</span><br><span class="line">      logBuffer(video);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 播放结束重置卡顿计数器</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'ended'</span>) &#123;</span><br><span class="line">      video._buffer_times = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>注意在视频播放结束的时候，我们要把 <code>_buffer_times</code> 计数器重置为 0，否则在该视频二次播放的时候，初次加载就被认为是卡顿。</p><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><p>当卡顿超过一定时长时（暂定 1 秒），用户可能会失去耐心关闭页面或者刷新页面，如果我们一味的等待视频缓冲完再统计，可能会丢失这种情况的数据。另外实际上当卡顿时间超过忍耐时长后，再统计具体的时间已经没有太大意义了。因此我们可以对上面的方案进行优化，增加卡顿超时直接统计的逻辑。  </p><p>原理也很简单，在 <code>waiting</code> 时设置一个定时器与 <code>playing</code> 监听器回调竞争：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> BufferTimeout = <span class="number">1000</span>; <span class="comment">// 卡顿超时阈值</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> logBuffer = <span class="function">(<span class="params">video, timeout</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (video._buffer_reported) &#123;</span><br><span class="line">    video._buffer_reported = <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  video._buffer_times += <span class="number">1</span>;</span><br><span class="line">  video._buffer_reported = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 初次缓冲为正常缓冲，不上报</span></span><br><span class="line">  <span class="keyword">if</span> (video._buffer_times === <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 超时上报</span></span><br><span class="line">  <span class="keyword">if</span> (timeout) &#123;</span><br><span class="line">    logger.log(<span class="string">'Buffer '</span>, [BufferTimeout]);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 正常上报</span></span><br><span class="line">  <span class="keyword">if</span> (video._waiting_time &amp;&amp; video._playing_time) &#123;</span><br><span class="line">    <span class="keyword">const</span> bufferCost = video._playing_time - video._waiting_time;</span><br><span class="line">    logger.log(<span class="string">'Buffer'</span>, [bufferCost]);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">video._buffer_times = <span class="number">0</span>;</span><br><span class="line">video._buffer_reported = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">[<span class="string">'waiting'</span>, <span class="string">'playing'</span>, <span class="string">'ended'</span>].forEach(<span class="function">(<span class="params">eventName</span>) =&gt;</span> &#123;</span><br><span class="line">  video.addEventListener(eventName, () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> key = <span class="string">`_<span class="subst">$&#123;eventName&#125;</span>_time`</span>;</span><br><span class="line">    video[key] = <span class="built_in">Date</span>.now();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 加载超时直接记录</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'waiting'</span>) &#123;</span><br><span class="line">      setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">        logBuffer(video, <span class="literal">true</span>);</span><br><span class="line">      &#125;, BufferTimeout);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在 playing 时记录卡顿</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'playing'</span>) &#123;</span><br><span class="line">      logBuffer(video);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 播放结束重置卡顿计数器</span></span><br><span class="line">    <span class="keyword">if</span> (eventName === <span class="string">'ended'</span>) &#123;</span><br><span class="line">      video._buffer_times = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>其实这里的超时逻辑还可以用 <code>Promise.race</code> 来实现，不过现在这样通过 Flag 来控制竞态也是没啥问题的。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events" target="_blank" rel="noopener">Media events - MDN</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;业务中使用到视频播放后，一些不确定的因素例如用户端网络异常、CDN 异常等等，导致视频加载缓慢和发生卡顿，这些质量问题会给用户体验带来较大的伤害，也会影响产品的留存转化率。因此对线上视频进行质量监控意义很大，可以让我们明确知道用户端的异常发生概率，以便做进一步的优化。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="视频" scheme="https://blog.daraw.cn/tags/%E8%A7%86%E9%A2%91/"/>
    
      <category term="质量监控" scheme="https://blog.daraw.cn/tags/%E8%B4%A8%E9%87%8F%E7%9B%91%E6%8E%A7/"/>
    
  </entry>
  
  <entry>
    <title>前端函数式编程</title>
    <link href="https://blog.daraw.cn/2017/08/16/fp-with-fe/"/>
    <id>https://blog.daraw.cn/2017/08/16/fp-with-fe/</id>
    <published>2017-08-16T22:19:15.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>前言：这个其实是我最近打算在在团队做的一个分享，这个分享将会聊聊函数式编程在前端的一些成熟的应用以及尝试。</p><a id="more"></a><h2 id="Something-interesting"><a href="#Something-interesting" class="headerlink" title="Something interesting"></a>Something interesting</h2><h3 id="罗素悖论"><a href="#罗素悖论" class="headerlink" title="罗素悖论"></a>罗素悖论</h3><p>在讲罗素悖论之前，先提一个小的需求，看看大家能不能实现：</p><blockquote><p>设计出一个函数<code>f</code>，接受一个函数<code>g</code>作为参数，返回布尔值。<br>函数<code>f</code>判断<code>g</code>会不会导致死循环，会则返回<code>true</code>，不会则返回false。</p></blockquote><p>如果学过离散数学的同学可能就会想起来了，这便是著名的<strong>图灵机停机问题</strong>。这个需求看似描述很完备，有输入输出的要求，也有函数功能的要求，但这个需求是不可实现的。<br>证明很简单：假设存在满足上述要求的函数<code>f</code>，那么我们定义一个邪恶的函数<code>evil</code>，其伪代码定义如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">evil</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (f(evil) == <span class="literal">true</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>) &#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>于是这里便产生了悖论：如果<code>f</code>判定<code>evil</code>不会死循环，<code>evil</code>就会死循环；反之则不会死循环。所以假设不成立，即不存在符合要求的<code>f</code>。  </p><p>类似的悖论还有著名的理发师悖论：理发师给所有不给自己理发的人理发。<br>不可解的<code>停机问题</code>其实便是<strong>哥德尔不完备定理</strong>的一种形式。</p><h3 id="Lambda-λ-演算"><a href="#Lambda-λ-演算" class="headerlink" title="Lambda (λ) 演算"></a>Lambda (λ) 演算</h3><blockquote><p>Lambda演算可以被称为最小的通用程序设计语言。它包括一条变换规则（变量替换）和一条函数定义方式，Lambda演算之通用在于，任何一个可计算函数都能用这种形式来表达和求值。因而，它是等价于图灵机的。尽管如此，Lambda演算强调的是变换规则的运用，而非实现它们的具体机器。可以认为这是一种更接近软件而非硬件的方式。 –from Wikipedia</p></blockquote><p>这里我不会去讲太多关于Lambda演算的内容，一个原因是在没有一些相关的数理逻辑的前置知识的前提下很难三言两语之内将其描述清楚，另一个原因是我自己也不是很懂（笑。  </p><p>那么为什么会提到Lambda演算呢？在图灵机的停机问题之前，有一个更加有趣的问题：能否判断两个lambda演算表达式是否等价，这个问题和图灵机停机问题同属于判定性问题。邱奇运用λ演算在1936年给出判定性问题一个否定的答案。<br>最重要的，Lambda演算对函数式编程语言有巨大的影响，比如Lisp语言、ML语言和Haskell语言。而通过这些，我们将引入今天的主要话题：函数式编程。但是注意的是，今天我讲的将会是偏前端方向的应用，而不会去过度的探讨演算、或者是范畴论等知识。</p><h2 id="语言层面"><a href="#语言层面" class="headerlink" title="语言层面"></a>语言层面</h2><p>首先放出我的观点：<strong>JavaScriprt 适合也不适合函数式编程</strong>。</p><h3 id="适合函数式编程"><a href="#适合函数式编程" class="headerlink" title="适合函数式编程"></a>适合函数式编程</h3><p>JavaScript是一门多范式的语言，我们很难去说JS到底是面向过程还是面向对象的语言，甚至我们也可以说JS就是一门函数式语言。但是这一切并不是矛盾的，OOP与FP完全可以结合起来使用。  </p><p>对于函数式语言的定义其实没有明确的界限，没有说一定要有Monad，或者是Hindley-Milner类型系统等等。JS中函数是一等对象，所以完全可以认为JS是一门函数式编程语言，而且JS的灵活性允许我们去模拟常见函数式语言中的大多数操作。</p><ul><li><p>纯函数 无副作用</p><blockquote><p>此函数在相同的输入值时，需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关，也和由I/O设备产生的外部输出无关。<br>该函数不能有语义上可观察的函数副作用，诸如“触发事件”，使输出设备输出，或更改输出值以外物件的内容等。</p></blockquote></li><li><p>不可变数据</p><blockquote><p>Immutable Data是指一旦被创造后，就不可以被改变的数据。</p></blockquote><p>也许大家会产生疑惑，没有变量应用怎么跑起来？这里注意的是虽然没有变量，但是会有绑定的概念，数据虽然不可变，但是绑定可变。  </p><p>JS本身提供了一个<code>Object.freeze()</code>方法，但是类似浅拷贝，它只是一层浅<code>freeze</code>，所以如果真的要去<code>freeze</code>一个对象，必须一层层递归<code>freeze</code>处理。  </p><p>这样做虽然可以实现不可变，但是无论做不做<code>freeze</code>处理，每次都产生一个新的对象会带来大量的中间变量，这一点后面我讲到Redux的时候也会提到，如果使用过Redux，相信应该知道这个问题，<code>Object.assign</code>会带来大量的中间对象，影响性能，而解决方案就是Immutable.js。<br>Immutable.js是一个不可变数据结构的JS库，它的原理其实也很简单，借助字典树，共享了不变的部分。有兴趣的同学可以读一读一篇著名的文章《Understanding Clojure’s Persistent Vectors》，Immutable.js的实现原理与Clojure的数据结构底层实现非常相似。下面的Redux环节我还会提到这个问题。</p></li><li><p>递归<br>循环和循环是常见的流程控制方式，在绝大多数场景下两者是可以等价互换的。<br>递归的优点在于代码简洁、清晰，并且容易验证正确性。在一些函数式语言中，即使存在流程控制语句<code>if</code> <code>else</code>等等，他们也只是函数的语法糖。  </p></li><li><p>curry / compose / …<br>这些虽然JS没有直接提供，但是已经有足够多的第三方库可以来做这个工作，例如 Underscore / Lodash / Ramda。这些函数的实现也可以阅读上述库的源码来学习。</p></li><li><p>惰性求值<br>惰性求值可以最小化计算机要做的工作。JS中有着一小部分表达式是属于惰性求值的，例如<code>||</code>和<code>&amp;&amp;</code>，这也是所谓的“短路原理”：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> i = <span class="number">1</span></span><br><span class="line"><span class="literal">true</span> || i++</span><br><span class="line"><span class="literal">false</span> &amp;&amp; i++</span><br><span class="line"><span class="built_in">console</span>.log(i) <span class="comment">// 1</span></span><br></pre></td></tr></table></figure><p>但是语言直接提供的这点惰性实在是太可怜了，JS中也基本都是立即求值。<br>这里拿Haskell举个例子：</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Prelude</span>&gt; <span class="keyword">import</span> Data.List</span><br><span class="line"><span class="type">Prelude</span> <span class="type">Data</span>.<span class="type">List</span>&gt; take <span class="number">10</span> $ sort [<span class="number">100</span>, <span class="number">99.</span><span class="number">.0</span>] :: [<span class="type">Int</span>]</span><br><span class="line"><span class="comment">-- [0,1,2,3,4,5,6,7,8,9]</span></span><br></pre></td></tr></table></figure><p>简单介绍下，<code>[Int]</code>是类型标注，表示返回的是一个整型列表；<code>[100, 99..0]</code>代表生成一个等差的列表；<code>sort</code>是排序；<code>$</code>是一个中缀函数，可以理解为把右边部分括起来。<br>这段代码的意思是生成一个100到0的等差数列，把这个等差列表从小到大排序并取出前十个。如果在JavaScript中的等价的代码应该类似如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = []</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">100</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">  arr.push(i)</span><br><span class="line">&#125;</span><br><span class="line">arr.sort(<span class="function">(<span class="params">a, b</span>) =&gt;</span> a - b)</span><br><span class="line"><span class="built_in">console</span>.log(arr.splice(<span class="number">0</span>, <span class="number">10</span>))</span><br><span class="line"><span class="comment">// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]</span></span><br></pre></td></tr></table></figure><p>无论是什么JS引擎，这段JS代码在执行的时候都会把这个数组的所有元素都遍历一遍；相反的是上面的Haskell代码在执行的时候，Haskell的编译器GHC只会尝试去计算刚好足够取出需要的10个元素为止。所以，在Haskell中，我们完全可以定义出无限长度的数组，然后设法取出其中的一部分，GHC也会在底层通过一些黑魔法来帮助实现。  </p><p>让我们回到JS：在ES6之前，模拟惰性求值的方式一般是thunk函数，或者借助Stream抽象。所幸ES6使得JS拥有了强大的<code>generator</code>函数，我们现在还可以借助它模拟惰性求值。  </p><p>具体的实现可以阅读参考文献中的文章，也有现成的库例如Lazy.js可以使用，这里不再赘述。  </p><p>但是注意的是，惰性求值可以为我们带来较好的性能，也可以导致性能下降。大量的延迟求值，可能会带来内存的消耗积压。Haskell中这个现象称为任务堆积，所以即便是默认惰性求值的Haskell，也为使用者提供了强制立即求值的方法，有兴趣的同学可以自行学习一些Haskell的知识。</p></li></ul><h3 id="不适合函数式编程"><a href="#不适合函数式编程" class="headerlink" title="不适合函数式编程"></a>不适合函数式编程</h3><ul><li><p>递归<br>在JS中，递归容易导致一个致命的问题：<strong>爆栈</strong>。<br>尾递归优化可以使得尾递归在编译时被优化为循环从而避免爆栈，但是到目前为止，JS是没有什么正儿八经的尾递归优化的，当然我们有着<code>Trampolining</code>这样的奇技淫巧可以巧妙的避免爆栈，但是每一层的转换消耗的匿名函数开销也是不可小觑：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">trampoline</span>(<span class="params">f</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> result = f.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line">    <span class="keyword">while</span> (result <span class="keyword">instanceof</span> <span class="built_in">Function</span>) &#123;</span><br><span class="line">      result = result();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> reduce = trampoline(<span class="function"><span class="keyword">function</span> <span class="title">myself</span>(<span class="params">f, list, sum</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (list.length &lt; <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> sum;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">      <span class="keyword">var</span> val = list.shift();</span><br><span class="line">      <span class="keyword">return</span> myself(f, list, f(val, list));</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>上面这段代码来自贺老在今年的FP China会议上的分享STC VS PTC。<br>ES6早已规定了JS应该有尾递归的优化，但是大家迟迟不去实现，主要是尾递归优化的STC与PTC之争：<br>PTC:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">sum</span>(<span class="params">n, total = <span class="number">0</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span>) <span class="keyword">return</span> total</span><br><span class="line">  <span class="keyword">else</span> <span class="keyword">return</span> sum(n - <span class="number">1</span>, total + n)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>PTC目前只有Safari支持，V8可以通过flag开启。STC的问题在于不知道写对了没有，开发时不爆栈不代表生产环境不爆栈，以及很难以调试。<br>STC:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">factorial</span>(<span class="params">n, acc = <span class="number">1</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> acc;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">continue</span> factorial(n - <span class="number">1</span>, acc * n)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>STC通过新的语法，保证不写对就会报错，但是新的语法会引入维护等一些问题。<br>所以因为STC与PTC之争，至今JS仍没有一个可靠的尾递归优化。</p></li><li><p>纯函数难以保证<br>在Haskell、Clojure等语言中，会有语言或者是编译的层面去做纯度的保障，但是在JS中，这一切只能靠我们自己来做约束，在多人合作的时候很难保证不会有坑。</p></li><li><p>弱类型，难以模拟类型系统</p></li><li><p>没有 monadic io</p></li><li><p>。。。</p></li></ul><h2 id="架构层面"><a href="#架构层面" class="headerlink" title="架构层面"></a>架构层面</h2><p>事实上，随着RxJS、React全家桶等受函数式影响的库的崛起，前端可以说是工程里应用函数式最为成熟的领域之一了。目前应用函数式比较广泛的领域还有金融（Haskell、Erlang等等），大数据（Scala）。</p><h3 id="React全家桶"><a href="#React全家桶" class="headerlink" title="React全家桶"></a>React全家桶</h3><p>首先，<strong>这不是一个React全家桶相关的入门介绍文章</strong>，理解接下来的内容期望你至少有一些React的相关开发经验，有一些Redux的开发经验就更棒了！</p><ul><li><p>React<br>先说说React，提及React，大家想到的往往是V-DOM，以至于Vue2在引入V-DOM后，两者已经极其相似了，事实上两者在设计思想上有一个很大的区别，在于React更加追求纯度，Vue的作者尤雨溪也曾明确表示Vue并不像React那样追求纯度。</p><p>相信大家都知道语法糖，这是指简化的语法，鼓励大家使用，例如ES6的Class、Arrow Function等等都是语法糖。那么不知道你有没有思考过，为什么React的生命周期函数的名字那么长？为什么React直接输出HTML的API名字<code>dangerouslySetInnerHTML</code>那么长？<br>这其实是一个很古老的API设计，叫做<strong>语法盐</strong>，与语法糖相反，它设计出让人感觉难用的API，让使用者在使用前多加思考是不是真的需要使用这些API，从而避免写出低质量的代码设计。</p><p>既然React不鼓励使用生命周期函数，那么React鼓励的是什么呢？答案是<strong>Pure Component</strong>。<br>React鼓励你去从一个不变的角度思考组件，如果你的组件无须设定生命周期钩子函数，那么你的组件类可以继承<code>PureComponent</code>而非普通的<code>Component</code>，<code>PureComponent</code>会自动去做<code>shouldComponentUpdate()</code>的判断优化。<br>但是<code>PureComponent</code>并非万能的灵药，先抛去其浅对比（shallowly compare）<code>state / prop</code>的缺陷不谈，一个应用怎么可能避免不了状态呢？React并不管，它只管给定状态就能做到幂等渲染，虽然保证了自身的纯度，却把状态的问题抛回给了用户。</p></li><li><p>Redux<br>这是社区对React应用的数据层解决方案。这里我也不会去讲Flux架构入门基础，更不会去讲其被讲烂了的Pub/Sub设计模式。</p><p>接触过Redux的一定会对Redux中反复提及<code>reducer</code>纯函数、<code>state</code>不可变等函数式的概念印象深刻；如果读过Redux的源码，你会发现Redux内部通过<code>compose</code>以极低的代码成本实现了洋葱模型的中间件。</p><p>除去这些函数式风格的API，和函数式的实现，更重要的是Redux实现了一套CQRS + ES软件架构。<br>CQRS全称Command Query Responsibility Segregation，即命令查询职责分离；ES全称Event Sourcing，即事件溯源。为什么会提CQRS + ES架构呢？因为目前为止，可以看到的基于函数式的大型项目至少一半都是基于这套架构，</p><p>下面我简单的讲一下这一套架构，CQRS其实就是读写分离，相信了解过数据库的同学不会对这个概念陌生，在分布式数据库中，通常借助读写分离来保证性能，这也是CQRS最常见的应用场景之一；ES则是不保存对象的最新状态，而是保存对象产生的所有事件，通过事件溯源（Event Sourcing，ES）得到对象最新状态。在Redux中，C端采用Event Sourcing的技术，在EventStore中存储事件；Q端存储对象的最新状态，用于提供查询支持。</p><p>ES近年来在并发编程领域应用的也非常多，例如Scala异步编程库Akka，基于Actor异步编程模型结合了ES。这种架构可以避免状态的竞争和锁，以少量的实时性损耗换取了可靠性，不会出现同时更新一个数据的问题。但是现在前端的复杂度还没有达到分布式场景下后端及数据库的程度，所以现在可能还不能去感受到这套架构带来的好处。</p><p>但是Redux借助CQRS+ES，带来的一个直观的好处就是调试工具的使用，相信使用过的同学一定对时光旅行这个功能印象深刻，可以随意的调节得到每个时刻的应用状态。其实现依赖于在Redux中<code>Action</code>就是<code>Event</code>，<code>Reducer</code>就是状态转换，根据初始<code>State</code>和<code>Event</code>记录，可以推算出每一个时刻的状态。</p><p>说了这么多，Redux带来的坏处是什么呢？主要有两点：<br>第一点，在实际的开发中，我们的操作并非都是同步的，几乎是到处充斥着异步操作，而Redux为了保证自身的纯度，把不可靠的异步问题再一次抛给了用户；第二点，JS的数据结构并非天生的不可变，<code>reducer</code>函数为了追求不可变，大量使用<code>Object.assign()</code>来创建新的状态对象（例如下面这段代码，来自Redux文档），所以会产生很多的中间状态变量，带来性能问题：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">todoApp</span>(<span class="params">state = initialState, action</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">switch</span> (action.type) &#123;</span><br><span class="line">    <span class="keyword">case</span> SET_VISIBILITY_FILTER:</span><br><span class="line">      <span class="keyword">return</span> <span class="built_in">Object</span>.assign(&#123;&#125;, state, &#123;</span><br><span class="line">        visibilityFilter: action.filter</span><br><span class="line">      &#125;)</span><br><span class="line">    <span class="keyword">case</span> ADD_TODO:</span><br><span class="line">      <span class="keyword">return</span> <span class="built_in">Object</span>.assign(&#123;&#125;, state, &#123;</span><br><span class="line">        todos: [</span><br><span class="line">          ...state.todos,</span><br><span class="line">          &#123;</span><br><span class="line">            text: action.text,</span><br><span class="line">            completed: <span class="literal">false</span></span><br><span class="line">          &#125;</span><br><span class="line">        ]</span><br><span class="line">      &#125;)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">      <span class="keyword">return</span> state</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于上面两点，第一点社区的解决方案是redux-thunk、redux-saga、redux-observable，第二点则是Immutable.js。</p></li><li><p>redux-thunk，redux-saga，redux-observable<br>这一块其实我不是很熟悉，也没有真正在业务中尝试使用过这些方案，就不献丑了，推荐阅读参考资料里的《Redux Thunk vs Saga vs Observable》，当然也期待有同学能够积极分享自己的经验。</p></li><li><p>Immutable.js<br>Immutable.js事实上是和React一起出来的，只是React的光芒掩盖了Immutable.js，直到Redux引入了性能问题，大家才想到了Immutable.js。其实关于Immutable.js也没啥好介绍的，API看官网即可，其实现则可以参考阅读Clojure的数据结构实现。</p></li></ul><h3 id="Reactive-Programming-响应式编程"><a href="#Reactive-Programming-响应式编程" class="headerlink" title="Reactive Programming 响应式编程"></a>Reactive Programming 响应式编程</h3><p>讲FP牵扯到RP很大的原因是因为大多数场景下RP和FP息息相关着。<br>这一块我还欠缺很多，所以暂时不讲太多。</p><ul><li><p>FRP vs F&amp;RP<br>两者区别在于：FRP是基于时间连续的，RP是基于时间离散的。</p></li><li><p>Rx.js<br>Rx不是FRP，而是F&amp;RP，因为Rx并不强调时间的作用，只是借助了函数式编程的一些思想让API变得更加优雅。<br>为什么我们会需要Rx.js呢？推荐阅读《单页应用的数据流方案探索》</p></li><li><p>Cycle.js<br>循环依赖问题<br>函数不动点模型</p></li><li><p>Elm<br>受Haskell影响<br>影响了Redux</p></li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><h2 id="递归"><a href="#递归" class="headerlink" title="递归"></a>递归</h2><ul><li><a href="http://johnhax.net/2017/stc-vs-ptc/#0" target="_blank" rel="noopener">STC vs PTC</a></li><li><a href="https://zhuanlan.zhihu.com/p/26941235" target="_blank" rel="noopener">[译]All About Recursion, PTC, TCO and STC in JavaScript</a></li></ul><h2 id="惰性求值"><a href="#惰性求值" class="headerlink" title="惰性求值"></a>惰性求值</h2><ul><li><a href="https://zhuanlan.zhihu.com/p/26535479" target="_blank" rel="noopener">如何用 JavaScript 实现一个数组惰性求值库</a></li><li><a href="https://zhuanlan.zhihu.com/p/26587745" target="_blank" rel="noopener">js中的Stream实现</a></li></ul><h2 id="CQRS"><a href="#CQRS" class="headerlink" title="CQRS"></a>CQRS</h2><ul><li><a href="https://martinfowler.com/bliki/CQRS.html" target="_blank" rel="noopener">CQRS</a></li><li><a href="https://github.com/reactjs/redux/issues/351" target="_blank" rel="noopener">Redux and it’s relation to CQRS (and other things)</a></li><li><a href="https://www.slideshare.net/ktoso/akka-persistence-event-sourcing-in-30-minutes" target="_blank" rel="noopener">Akka persistence == event sourcing in 30 minutes</a></li></ul><h2 id="Redux异步解决方案"><a href="#Redux异步解决方案" class="headerlink" title="Redux异步解决方案"></a>Redux异步解决方案</h2><ul><li><a href="http://slides.com/dabit3/deck-11-12#/" target="_blank" rel="noopener">Redux Thunk vs Saga vs Observable</a></li></ul><h2 id="不可变数据"><a href="#不可变数据" class="headerlink" title="不可变数据"></a>不可变数据</h2><ul><li><a href="https://www.zhihu.com/question/53804334" target="_blank" rel="noopener">函数式编程所倡导使用的「不可变数据结构」如何保证性能？</a></li><li><a href="http://hypirion.com/musings/understanding-persistent-vector-pt-1" target="_blank" rel="noopener">Understanding Clojure’s Persistent Vectors, pt. 1</a></li><li><a href="http://hypirion.com/musings/understanding-persistent-vector-pt-2" target="_blank" rel="noopener">Understanding Clojure’s Persistent Vectors, pt. 2</a></li><li><a href="http://hypirion.com/musings/understanding-persistent-vector-pt-3" target="_blank" rel="noopener">Understanding Clojure’s Persistent Vectors, pt. 3</a></li></ul><h2 id="Rx"><a href="#Rx" class="headerlink" title="Rx"></a>Rx</h2><ul><li><a href="https://github.com/ReactiveX/reactivex.github.io/issues/130" target="_blank" rel="noopener">Clarify the differences between Functional Reactive Programming and Reactive Extensions</a></li><li><a href="https://zhuanlan.zhihu.com/p/23331432" target="_blank" rel="noopener">Hello RxJS</a></li></ul><h2 id="综合"><a href="#综合" class="headerlink" title="综合"></a>综合</h2><ul><li><a href="https://www.zhihu.com/question/59871249/answer/170400954" target="_blank" rel="noopener">前端开发js函数式编程真实用途体现在哪里？</a></li><li><a href="https://zhuanlan.zhihu.com/p/24076438" target="_blank" rel="noopener">为什么说 JavaScript 不擅长函数式编程</a></li><li><a href="https://zhuanlan.zhihu.com/p/26426054" target="_blank" rel="noopener">单页应用的数据流方案探索</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前言：这个其实是我最近打算在在团队做的一个分享，这个分享将会聊聊函数式编程在前端的一些成熟的应用以及尝试。&lt;/p&gt;
    
    </summary>
    
    
      <category term="FP" scheme="https://blog.daraw.cn/categories/FP/"/>
    
    
      <category term="React" scheme="https://blog.daraw.cn/tags/React/"/>
    
      <category term="FP" scheme="https://blog.daraw.cn/tags/FP/"/>
    
      <category term="Redux" scheme="https://blog.daraw.cn/tags/Redux/"/>
    
  </entry>
  
  <entry>
    <title>多说一句再见</title>
    <link href="https://blog.daraw.cn/2017/03/23/goodbye-duoshuo/"/>
    <id>https://blog.daraw.cn/2017/03/23/goodbye-duoshuo/</id>
    <published>2017-03-23T20:01:15.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>因公司业务调整，非常遗憾的向大家宣布多说项目即将关闭。 我们将于2017年6月1日正式关停服务，在此之前您可以通过后台的数据导出功能导出自己站点的评论数据。 对此给您造成的不便，我们深表歉意，感谢您的一路相伴。</p></blockquote><p>从博客搭建好以来一直使用着多说作为评论系统，虽然多说有着不少的黑点，比如上了HTTPS后失去地址栏绿锁，也听说过多说有各种各样的bug，服务器故障导致评论框不出现更是没少见过，不过以后再也不能黑多说了。  </p><a id="more"></a><p>由于不是很想再去使用国内其他的评论系统了，博客近期也没有迁移到非静态的博客系统的计划，所以把评论系统换成了Disqus。至于GFW的问题，我觉得科学上网是一个程序员必备的技能，Disqus被墙不应该会影响本博客的访客们。  </p><p>另外可能是多说导出数据的格式的问题，在迁移到Disqus的过程中一直没法把转好的数据导入Disqus，所以很遗憾以前的评论都丢失了，最近也比较忙，过阵子闲下来我再尝试看看能不能把数据找回来。  </p><p>感谢多说这一年半的陪伴，也感谢曾经在博客留言的朋友们。<br>如果可以，我还是很乐意为多说团队买一杯咖啡的。  </p><p>最后，再多说一句再见！</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;因公司业务调整，非常遗憾的向大家宣布多说项目即将关闭。 我们将于2017年6月1日正式关停服务，在此之前您可以通过后台的数据导出功能导出自己站点的评论数据。 对此给您造成的不便，我们深表歉意，感谢您的一路相伴。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从博客搭建好以来一直使用着多说作为评论系统，虽然多说有着不少的黑点，比如上了HTTPS后失去地址栏绿锁，也听说过多说有各种各样的bug，服务器故障导致评论框不出现更是没少见过，不过以后再也不能黑多说了。  &lt;/p&gt;
    
    </summary>
    
    
    
      <category term="多说" scheme="https://blog.daraw.cn/tags/%E5%A4%9A%E8%AF%B4/"/>
    
  </entry>
  
  <entry>
    <title>《你不知道的JS上卷》阅读小记之setTimeout的this指向问题</title>
    <link href="https://blog.daraw.cn/2017/02/19/notes-about-you-dont-know-js-settimeout-this/"/>
    <id>https://blog.daraw.cn/2017/02/19/notes-about-you-dont-know-js-settimeout-this/</id>
    <published>2017-02-19T10:10:05.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>这几天翻看了下被传的神乎其神的《你不知道的JS》这本书，其实以前就看过一次，不过当时的level并不高，而且感觉这本书讲的有点绕，所以看了一点就没坚持下去。<br>这次翻看感觉还是比较轻松的，有些地方写的很好，有的地方还是感觉讲的有点绕（可能是翻译的问题），但总的来说这本书还是很不错的，基本都是JS中有坑、新手难以理解的点，简直就是《JS：The Bad Parts》（哈，开个玩笑~）。<br>这个小记不是打算记录书中内容的笔记，而是想补充纠正书中的讲的不完善的地方。</p><a id="more"></a><h2 id="this的问题"><a href="#this的问题" class="headerlink" title="this的问题"></a>this的问题</h2><p>在第二部分<code>2.2.2隐式绑定</code>一节中，提到了<code>setTimeout</code>的传入函数this的问题，书里说传入回调函数在执行的时候<code>context</code>为全局对象，所以<code>this</code>指向了全局对象：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="keyword">this</span>)</span><br><span class="line">&#125;, <span class="number">200</span>)</span><br></pre></td></tr></table></figure><p>在Chrome56中测试上面的一段代码确实输出为<code>window</code>全局对象，符合书中的描述，然而如果你在Node.js中测试这段代码你会发现输出是这样的：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">➜  Desktop  node -v</span><br><span class="line">v6.8.1</span><br><span class="line">➜  Desktop  node test.js</span><br><span class="line">Timeout &#123;</span><br><span class="line">  _called: true,</span><br><span class="line">  _idleTimeout: 200,</span><br><span class="line">  _idlePrev: null,</span><br><span class="line">  _idleNext: null,</span><br><span class="line">  _idleStart: 94,</span><br><span class="line">  _onTimeout: [Function],</span><br><span class="line">  _timerArgs: undefined,</span><br><span class="line">  _repeat: null &#125;</span><br></pre></td></tr></table></figure><p>What? 输出的是一个Timeout实例对象！<br>打开node的源码，在<code>node\timers.js</code>中有着<code>setTimeout</code>的实现，这里大概的讲一下，有兴趣的可以自己再去看看代码：<br>有一个<code>Timeout</code>构造函数，用来构造定时器对象，用一个链表存着所有的<code>Timeout</code>的实例对象，也就是每次执行暴露出来的<code>setTimeout</code>都会在链表中插入一个<code>Timeout</code>实例<code>timer</code>。下面是<code>Timeout</code>构造函数的代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Timeout</span>(<span class="params">after, callback, args</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">this</span>._called = <span class="literal">false</span>;</span><br><span class="line">  <span class="keyword">this</span>._idleTimeout = after;</span><br><span class="line">  <span class="keyword">this</span>._idlePrev = <span class="keyword">this</span>;</span><br><span class="line">  <span class="keyword">this</span>._idleNext = <span class="keyword">this</span>;</span><br><span class="line">  <span class="keyword">this</span>._idleStart = <span class="literal">null</span>;</span><br><span class="line">  <span class="keyword">this</span>._onTimeout = callback;</span><br><span class="line">  <span class="keyword">this</span>._timerArgs = args;</span><br><span class="line">  <span class="keyword">this</span>._repeat = <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>定时的部分<code>TimerWrap</code>则由C++来做处理，这里不是现在我们关注的关键点也脱离了JS的范畴暂且不细谈，在定时结束后，通过<code>ontimeout</code>函数来处理<code>timer</code>对象：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ontimeout</span>(<span class="params">timer</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> args = timer._timerArgs;</span><br><span class="line">  <span class="keyword">var</span> callback = timer._onTimeout;</span><br><span class="line">  <span class="keyword">if</span> (!args)</span><br><span class="line">    callback.call(timer);</span><br><span class="line">  <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> (args.length) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">        callback.call(timer, args[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">        callback.call(timer, args[<span class="number">0</span>], args[<span class="number">1</span>]);</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">        callback.call(timer, args[<span class="number">0</span>], args[<span class="number">1</span>], args[<span class="number">2</span>]);</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">default</span>:</span><br><span class="line">        callback.apply(timer, args);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (timer._repeat)</span><br><span class="line">    rearm(timer);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ontimeout</code>函数中正藏着<code>this</code>指向问题的真相：<code>callback.call(timer, ...)</code>。  </p><p>在JS中，<code>setTimeout</code>应该是属于<code>Event Loop</code>的<code>Macro Tasks</code>，与<code>I/O</code>等<code>Tasks</code>同等级，在浏览器中有<code>Web APIs</code>规范来定义这部分的实现，node没有（或者是我没找到，还请告知）。但我大概翻了下<code>Web APIs</code>的规范，也没有找到对<code>this</code> <code>context</code> 的规定。虽然不理解为什么node这样做，但是好歹也找出了与Chrome浏览器不同的原因。  </p><p>所以，关于<code>setTimeout</code>传入函数的<code>this</code>，我的建议是即使你写的代码只会在浏览器里运行，也最好不要依赖<code>this</code>会自动绑定到全局对象上去，而是应该手动借助<code>bind</code>绑定。当然使用ES6的箭头函数是没什么问题的，因为没有创建新的<code>context</code>，<code>this</code>都会毫无疑问的绑定在当前的<code>context</code>上：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> that = <span class="keyword">this</span></span><br><span class="line"></span><br><span class="line">setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line"><span class="built_in">console</span>.log(<span class="keyword">this</span> == that) <span class="comment">// true</span></span><br><span class="line">&#125;, <span class="number">200</span>)</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这几天翻看了下被传的神乎其神的《你不知道的JS》这本书，其实以前就看过一次，不过当时的level并不高，而且感觉这本书讲的有点绕，所以看了一点就没坚持下去。&lt;br&gt;这次翻看感觉还是比较轻松的，有些地方写的很好，有的地方还是感觉讲的有点绕（可能是翻译的问题），但总的来说这本书还是很不错的，基本都是JS中有坑、新手难以理解的点，简直就是《JS：The Bad Parts》（哈，开个玩笑~）。&lt;br&gt;这个小记不是打算记录书中内容的笔记，而是想补充纠正书中的讲的不完善的地方。&lt;/p&gt;
    
    </summary>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>ES6实现内部类的写法</title>
    <link href="https://blog.daraw.cn/2017/01/17/es6-static-class/"/>
    <id>https://blog.daraw.cn/2017/01/17/es6-static-class/</id>
    <published>2017-01-17T16:55:15.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>最近在把 <code>JIris</code> 移植到JS平台 <code>Iris.js</code> 的过程中不断的在Java和JS两种语言之间切换，ES6的 <code>Class</code> 某种程度来说可以写出更加优雅的代码，而不用去管背后的原型实现。但是不得不说有一个遗憾就是 ES6 虽然支持了静态方法，但是还不支持静态属性和静态类，于是仔细观察了下发现了几种ES6实现静态类的相对优雅的写法。</p><a id="more"></a><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>简化一个Demo，先是 Java 的写法：<br>** IrisValue.java **</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">IrisValue</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> mValue;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">IrisValue</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getValue</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> mValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setValue</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">        mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">int</span> test = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getTest</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> test;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setTest</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">            test = value;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span> <span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        IrisValue irisValue = <span class="keyword">new</span> IrisValue(<span class="number">10</span>);</span><br><span class="line">        IrisValue irisValue2 = <span class="keyword">new</span> IrisValue(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line">        irisValue.setValue(<span class="number">100</span>);</span><br><span class="line"></span><br><span class="line">        System.out.println(irisValue.getValue());</span><br><span class="line">        System.out.println(irisValue2.getValue());</span><br><span class="line"></span><br><span class="line">        Test testValue = <span class="keyword">new</span> Test();</span><br><span class="line">        Test testValue2 = <span class="keyword">new</span> Test();</span><br><span class="line"></span><br><span class="line">        System.out.println(testValue.getTest());</span><br><span class="line">        System.out.println(testValue2.getTest());</span><br><span class="line">        testValue.setTest(<span class="number">9</span>);</span><br><span class="line">        testValue2.setTest(<span class="number">99</span>);</span><br><span class="line">        System.out.println(testValue.getTest());</span><br><span class="line">        System.out.println(testValue2.getTest());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Test</code> 是<code>IrisValue</code>类的静态方法，并不属于<code>IrisValue</code>的实例对象。上面代码执行结果是：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">100</span><br><span class="line">10</span><br><span class="line">0</span><br><span class="line">0</span><br><span class="line">9</span><br><span class="line">99</span><br></pre></td></tr></table></figure><p>那么等同的JS代码应该怎么写呢？<br>ES6的Class本质还是函数，所以有一个很容易想到的写法：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IrisValue</span> </span>&#123;</span><br><span class="line">    <span class="keyword">constructor</span>(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span> value() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.mValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span> value(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line">    <span class="keyword">constructor</span>() &#123;</span><br><span class="line">        <span class="keyword">this</span>.mTest = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span> test() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.mTest;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span> test(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mTest = value;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">IrisValue.Test = Test;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> irisValue = <span class="keyword">new</span> IrisValue(<span class="number">10</span>);</span><br><span class="line"><span class="keyword">let</span> irisValue2 = <span class="keyword">new</span> IrisValue(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line">irisValue.value = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(irisValue.value);</span><br><span class="line"><span class="built_in">console</span>.log(irisValue2.value);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> testValue = <span class="keyword">new</span> IrisValue.Test();</span><br><span class="line"><span class="keyword">let</span> testValue2 = <span class="keyword">new</span> IrisValue.Test();</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(testValue.test);</span><br><span class="line"><span class="built_in">console</span>.log(testValue2.test);</span><br><span class="line">testValue.test = <span class="number">9</span>;</span><br><span class="line">testValue2.test = <span class="number">99</span>;</span><br><span class="line"><span class="built_in">console</span>.log(testValue.test);</span><br><span class="line"><span class="built_in">console</span>.log(testValue2.test);</span><br></pre></td></tr></table></figure><p>输出结果和上面Java（预想的）一样，静态属性目前也能这样实现。  </p><p>但是不要忘了<code>get/set</code>函数，所以应该有更加优雅的静态类和静态属性的写法：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IrisValue</span> </span>&#123;</span><br><span class="line">    <span class="keyword">constructor</span>(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span> value() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.mValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span> value(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">get</span> Test() &#123;</span><br><span class="line">    <span class="keyword">return</span> Test;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line">    <span class="keyword">constructor</span>() &#123;</span><br><span class="line">        <span class="keyword">this</span>.mTest = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span> test() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.mTest;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span> test(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mTest = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其实我们还能再<del>优雅</del>一点～</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IrisValue</span> </span>&#123;</span><br><span class="line">    <span class="keyword">constructor</span>(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span> value() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.mValue;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span> value(value) &#123;</span><br><span class="line">        <span class="keyword">this</span>.mValue = value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">get</span> Test() &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line">            <span class="keyword">constructor</span>() &#123;</span><br><span class="line">                <span class="keyword">this</span>.mTest = <span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">get</span> test() &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">this</span>.mTest;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">set</span> test(value) &#123;</span><br><span class="line">                <span class="keyword">this</span>.mTest = value;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其实上面的这些写法还是不够优雅，避免不了的牺牲了可读性。既然ES6已经引进了Class，期待以后能和Java一样去写静态类和静态属性。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近在把 &lt;code&gt;JIris&lt;/code&gt; 移植到JS平台 &lt;code&gt;Iris.js&lt;/code&gt; 的过程中不断的在Java和JS两种语言之间切换，ES6的 &lt;code&gt;Class&lt;/code&gt; 某种程度来说可以写出更加优雅的代码，而不用去管背后的原型实现。但是不得不说有一个遗憾就是 ES6 虽然支持了静态方法，但是还不支持静态属性和静态类，于是仔细观察了下发现了几种ES6实现静态类的相对优雅的写法。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
      <category term="ES6" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/ES6/"/>
    
    
      <category term="ES6" scheme="https://blog.daraw.cn/tags/ES6/"/>
    
      <category term="Class" scheme="https://blog.daraw.cn/tags/Class/"/>
    
  </entry>
  
  <entry>
    <title>IntelliJ IDEA在Linux下字体不正常解决方案</title>
    <link href="https://blog.daraw.cn/2016/11/24/intellij-idea-ugly-font-linux/"/>
    <id>https://blog.daraw.cn/2016/11/24/intellij-idea-ugly-font-linux/</id>
    <published>2016-11-24T20:34:15.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>之前遇到了一件奇怪的事，WebStorm中字体正常，IDEA直接导入WebStorm的设置备份也还是不行，如图所示。</p><a id="more"></a><p><img src="https://ww1.sinaimg.cn/large/005KE4htgw1f9hf9q1cy0j31hc0u0181.jpg" alt=""></p><p>本来这事也就放着不管了，昨天和Shaoxing聊天提到了这事，他提醒我正常情况下IDEA系列应该都是自带JDK的，于是我查了一下IDEA有带和不带JDK两个版本，自带JDK的会针对HiDPI和字体做一些优化。  </p><p>我打开了WS和IDEA进行对比，发现他们的About信息中的JVM版本果然不一样：<br><img src="https://ww1.sinaimg.cn/large/005KE4htgw1fa3hgndesmj30hy0b6q6l.jpg" alt=""><br><img src="https://ww1.sinaimg.cn/mw690/005KE4htgw1fa3hgo3t08j30ia0bkae3.jpg" alt="">  </p><p>在IDEA的设置里手动切换了JVM版本后IDEA会自动重启，然而并没什么卵用，重启后JVM又回到了Oracle版本。<br>我突然想起曾经在环境变量中配置过<code>IDEA_JDK</code>，于是删除了这个变量，在终端中输出这个变量已经不存在，然而还是不行。  </p><p>这时看到官网说可以在<code>idea.sh</code>中手动添加<code>IDEA_JDK</code>变量，于是我打开了<code>idea.sh</code>，其中60行往后为关键点：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ---------------------------------------------------------------------</span></span><br><span class="line"><span class="comment"># Locate a JDK installation directory which will be used to run the IDE.</span></span><br><span class="line"><span class="comment"># Try (in order): IDEA_JDK, idea.jdk, ../jre, JDK_HOME, JAVA_HOME, "java" in PATH.</span></span><br><span class="line"><span class="comment"># ---------------------------------------------------------------------</span></span><br><span class="line"><span class="keyword">if</span> [ -n <span class="string">"<span class="variable">$IDEA_JDK</span>"</span> -a -x <span class="string">"<span class="variable">$IDEA_JDK</span>/bin/java"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$IDEA_JDK</span>"</span></span><br><span class="line"><span class="keyword">elif</span> [ -s <span class="string">"<span class="variable">$HOME</span>/.IntelliJIdea2016.3/config/idea.jdk"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=`<span class="string">"<span class="variable">$CAT</span>"</span> <span class="variable">$HOME</span>/.IntelliJIdea2016.3/config/idea.jdk`</span><br><span class="line">  <span class="keyword">if</span> [ ! -d <span class="string">"<span class="variable">$JDK</span>"</span> ]; <span class="keyword">then</span></span><br><span class="line">    JDK=<span class="string">"<span class="variable">$IDE_HOME</span>/<span class="variable">$JDK</span>"</span></span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"><span class="keyword">elif</span> [ -x <span class="string">"<span class="variable">$IDE_HOME</span>/jre/jre/bin/java"</span> ] &amp;&amp; <span class="string">"<span class="variable">$IDE_HOME</span>/jre/jre/bin/java"</span> -version &gt; /dev/null 2&gt;&amp;1 ; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$IDE_HOME</span>/jre"</span></span><br><span class="line"><span class="keyword">elif</span> [ -n <span class="string">"<span class="variable">$JDK_HOME</span>"</span> -a -x <span class="string">"<span class="variable">$JDK_HOME</span>/bin/java"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$JDK_HOME</span>"</span></span><br><span class="line"><span class="keyword">elif</span> [ -n <span class="string">"<span class="variable">$JAVA_HOME</span>"</span> -a -x <span class="string">"<span class="variable">$JAVA_HOME</span>/bin/java"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$JAVA_HOME</span>"</span></span><br></pre></td></tr></table></figure><p>后面的代码省去，虽然没学过<code>shell</code>，但很明显查找JDK的流程为先查看环境变量有没有<code>IDEA_JDK</code>变量，如果没有再去看配置信息里有没有设置<code>idea.jdk</code>，如果没有再去找IDE的目录里自带的JDK。<br>所以解决方法很简单了，把上面的流程注释掉，直接去IDE的目录下找自带的JDK：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ---------------------------------------------------------------------</span></span><br><span class="line"><span class="comment"># Locate a JDK installation directory which will be used to run the IDE.</span></span><br><span class="line"><span class="comment"># Try (in order): IDEA_JDK, idea.jdk, ../jre, JDK_HOME, JAVA_HOME, "java" in PATH.</span></span><br><span class="line"><span class="comment"># ---------------------------------------------------------------------</span></span><br><span class="line"><span class="comment"># if [ -n "$IDEA_JDK" -a -x "$IDEA_JDK/bin/java" ]; then</span></span><br><span class="line"><span class="comment">#   JDK="$IDEA_JDK"</span></span><br><span class="line"><span class="comment"># elif [ -s "$HOME/.IntelliJIdea2016.3/config/idea.jdk" ]; then</span></span><br><span class="line"><span class="comment">#   JDK=`"$CAT" $HOME/.IntelliJIdea2016.3/config/idea.jdk`</span></span><br><span class="line"><span class="comment">#   if [ ! -d "$JDK" ]; then</span></span><br><span class="line"><span class="comment">#     JDK="$IDE_HOME/$JDK"</span></span><br><span class="line"><span class="comment">#   fi</span></span><br><span class="line"><span class="comment"># elif [ -x "$IDE_HOME/jre/jre/bin/java" ] &amp;&amp; "$IDE_HOME/jre/jre/bin/java" -version &gt; /dev/null 2&gt;&amp;1 ; then</span></span><br><span class="line"><span class="keyword">if</span> [ -x <span class="string">"<span class="variable">$IDE_HOME</span>/jre/jre/bin/java"</span> ] &amp;&amp; <span class="string">"<span class="variable">$IDE_HOME</span>/jre/jre/bin/java"</span> -version &gt; /dev/null 2&gt;&amp;1 ; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$IDE_HOME</span>/jre"</span></span><br><span class="line"><span class="keyword">elif</span> [ -n <span class="string">"<span class="variable">$JDK_HOME</span>"</span> -a -x <span class="string">"<span class="variable">$JDK_HOME</span>/bin/java"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$JDK_HOME</span>"</span></span><br><span class="line"><span class="keyword">elif</span> [ -n <span class="string">"<span class="variable">$JAVA_HOME</span>"</span> -a -x <span class="string">"<span class="variable">$JAVA_HOME</span>/bin/java"</span> ]; <span class="keyword">then</span></span><br><span class="line">  JDK=<span class="string">"<span class="variable">$JAVA_HOME</span>"</span></span><br></pre></td></tr></table></figure><p>保存后打开IDEA，果然一切都正常了，About信息中也显示使用了自带的JDK。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前遇到了一件奇怪的事，WebStorm中字体正常，IDEA直接导入WebStorm的设置备份也还是不行，如图所示。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://blog.daraw.cn/categories/Linux/"/>
    
    
      <category term="Intellij IDEA" scheme="https://blog.daraw.cn/tags/Intellij-IDEA/"/>
    
      <category term="Linux" scheme="https://blog.daraw.cn/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>node-thunkify源码阅读笔记</title>
    <link href="https://blog.daraw.cn/2016/11/11/notes-about-node-thunkify/"/>
    <id>https://blog.daraw.cn/2016/11/11/notes-about-node-thunkify/</id>
    <published>2016-11-11T16:00:15.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>Node7发布后已经可以通过添加<code>--harmony-async-await</code>的参数调用来直接支持<code>async/await</code>语法了，据说Node8还会进一步推进其发展，于是研究了一下JS的异步流程控制和下一代Node Web框架<code>Koa2</code>。  </p><p>关于<code>generator</code> <code>async/await</code>的发展史已有一大堆文章讲过了，这里不再赘述。<br>tj的<code>co</code>是<code>Koa2</code>上个大版本<code>Koa1</code>的核心，在没有<code>async/await</code>的时候一般会借助<code>co</code>来做自动流程控制。关于<code>co</code>的源码分析文章也有很多，代码不长值得一读，参考了一些分析文章也算是了解了其逻辑和思路。</p><p>在<code>co</code>中出现了一个<code>thunkToPromise</code>的函数，一些文章都跳过了这个并表示<code>thunk</code>函数已经没什么意义了，但本着好奇心读了阮一峰的<a href="http://www.ruanyifeng.com/blog/2015/05/thunk.html" target="_blank" rel="noopener">Thunk 函数的含义和用法</a>，文中一个地方一时没有搞懂，故写此文记录一下。</p><a id="more"></a><p><code>thunkify</code>的<a href="https://github.com/tj/node-thunkify/blob/master/index.js" target="_blank" rel="noopener">代码</a>很少，就是一个函数：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">thunkify</span>(<span class="params">fn</span>)</span>&#123;</span><br><span class="line">  assert(<span class="string">'function'</span> == <span class="keyword">typeof</span> fn, <span class="string">'function required'</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">var</span> args = <span class="keyword">new</span> <span class="built_in">Array</span>(<span class="built_in">arguments</span>.length);</span><br><span class="line">    <span class="keyword">var</span> ctx = <span class="keyword">this</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; args.length; ++i) &#123;</span><br><span class="line">      args[i] = <span class="built_in">arguments</span>[i];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params">done</span>)</span>&#123;</span><br><span class="line">      <span class="keyword">var</span> called;</span><br><span class="line"></span><br><span class="line">      args.push(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (called) <span class="keyword">return</span>;</span><br><span class="line">        called = <span class="literal">true</span>;</span><br><span class="line">        done.apply(<span class="literal">null</span>, <span class="built_in">arguments</span>);</span><br><span class="line">      &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        fn.apply(ctx, args);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">        done(err);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Demo：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">f</span>(<span class="params">a, b, callback</span>)</span>&#123;</span><br><span class="line">  <span class="keyword">var</span> sum = a + b;</span><br><span class="line">  callback(sum);</span><br><span class="line">  callback(sum);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> ft = thunkify(f);</span><br><span class="line">ft(<span class="number">1</span>, <span class="number">2</span>)(<span class="built_in">console</span>.log);</span><br><span class="line"><span class="comment">// 3</span></span><br></pre></td></tr></table></figure><p>让我一时没有搞懂的是为什么借助<code>called</code>标记能够确保回调函数只执行一次，借助VS Code的断点调试把文中示例的代码跑了一遍总算搞懂了：  </p><p>Demo中首先真正执行的是<code>thunkify(f)</code>，<code>f</code>函数传入<code>thunkify</code>后直接返回了一个闭包，这里称之为闭包1，闭包1被赋值给了ft，ft即为闭包1的一个引用。<br>接着执行的是<code>ft(1, 2)</code>，<code>ft</code>中传入了<code>(1, 2)</code>来执行，<code>ft</code>中将<code>duck type</code>的伪数组<code>arguments</code>保存为一个真实数组<code>args</code>，所以此时<code>args</code>数组中有两个成员即<code>1</code>和<code>2</code>，<code>ft</code>最后又返回了一个闭包，这里称之为闭包2。<br>接着执行的是<code>ft(1, 2)(console.log)</code>，也就是将<code>console.log</code>传入闭包2来执行，传入的<code>console.log</code>即形参<code>done</code>其实就是一开始<code>f</code>的回调函数，这时候重点来了，在闭包2中增加了一个标记<code>called</code>来记录回调是否执行过一次了，而<code>push</code>进<code>args</code>数组的函数则已经不是单纯的回调，而是被包裹了原回调、保证只会执行一次的函数</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (called) <span class="keyword">return</span>;</span><br><span class="line">  called = <span class="literal">true</span>;</span><br><span class="line">  done.apply(<span class="literal">null</span>, <span class="built_in">arguments</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后执行的就是<code>fn.apply(ctx, args)</code>，而<code>thunkify</code>函数的形参<code>fn</code>对应的就是一开始传入的<code>f</code>函数，所以总的看来，真正执行的还是最初的<code>f</code>函数，而被改变的是传入的回调，回调被包裹了一层，借助<code>called</code>标记来保证只会被执行一次，执行过后<code>called</code>标记被改变，不再会被执行。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Node7发布后已经可以通过添加&lt;code&gt;--harmony-async-await&lt;/code&gt;的参数调用来直接支持&lt;code&gt;async/await&lt;/code&gt;语法了，据说Node8还会进一步推进其发展，于是研究了一下JS的异步流程控制和下一代Node Web框架&lt;code&gt;Koa2&lt;/code&gt;。  &lt;/p&gt;
&lt;p&gt;关于&lt;code&gt;generator&lt;/code&gt; &lt;code&gt;async/await&lt;/code&gt;的发展史已有一大堆文章讲过了，这里不再赘述。&lt;br&gt;tj的&lt;code&gt;co&lt;/code&gt;是&lt;code&gt;Koa2&lt;/code&gt;上个大版本&lt;code&gt;Koa1&lt;/code&gt;的核心，在没有&lt;code&gt;async/await&lt;/code&gt;的时候一般会借助&lt;code&gt;co&lt;/code&gt;来做自动流程控制。关于&lt;code&gt;co&lt;/code&gt;的源码分析文章也有很多，代码不长值得一读，参考了一些分析文章也算是了解了其逻辑和思路。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;co&lt;/code&gt;中出现了一个&lt;code&gt;thunkToPromise&lt;/code&gt;的函数，一些文章都跳过了这个并表示&lt;code&gt;thunk&lt;/code&gt;函数已经没什么意义了，但本着好奇心读了阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2015/05/thunk.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Thunk 函数的含义和用法&lt;/a&gt;，文中一个地方一时没有搞懂，故写此文记录一下。&lt;/p&gt;
    
    </summary>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/categories/JavaScript/"/>
    
    
      <category term="thunkify" scheme="https://blog.daraw.cn/tags/thunkify/"/>
    
  </entry>
  
  <entry>
    <title>如何监听JS变量的变化</title>
    <link href="https://blog.daraw.cn/2016/08/17/how-to-monitor-changes-of-js-variable/"/>
    <id>https://blog.daraw.cn/2016/08/17/how-to-monitor-changes-of-js-variable/</id>
    <published>2016-08-17T13:48:39.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="https://www.zhihu.com/question/44724640" target="_blank" rel="noopener">如何监听 js 中变量的变化?</a><br>我现在有这样一个需求，需要监控js的某个变量的改变，如果该变量发生变化，则触发一些事件，不能使用timeinterval之类的定时去监控的方法，不知道有比较好的解决方案么？</p></blockquote><p>这个问题问的很好。  </p><p>流行的MVVM的JS库/框架都有共同的特点就是数据绑定，在数据变更后响应式的自动进行相关计算并变更DOM展现。所以这个问题也可以理解为<strong>如何实现MVVM库/框架的数据绑定</strong>。  </p><p>常见的数据绑定的实现有脏值检测，基于ES5的<code>getter</code>和<code>setter</code>，以及ES已被废弃的<code>Object.observe</code>，和ES6中添加的<code>Proxy</code>。</p><a id="more"></a><h2 id="脏值检测"><a href="#脏值检测" class="headerlink" title="脏值检测"></a>脏值检测</h2><p>angular使用的就是脏值检测，原理是比较新值和旧值，当值真的发生改变时再去更改DOM，所以angular中有一个<code>$digest</code>。那么为什么在像<code>ng-click</code>这样的内置指令在触发后会自动变更呢？原理也很简单，在<code>ng-click</code>这样的内置指令中最后追加了<code>$digest</code>。  </p><p>简易的实现一个脏值检测：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">"utf-8"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">title</span>&gt;</span>two-way binding<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">body</span> <span class="attr">onload</span>=<span class="string">"init()"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">button</span> <span class="attr">ng-click</span>=<span class="string">"inc"</span>&gt;</span></span><br><span class="line">            Increase</span><br><span class="line">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">button</span> <span class="attr">ng-click</span>=<span class="string">"reset"</span>&gt;</span></span><br><span class="line">            Reset</span><br><span class="line">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color:red"</span> <span class="attr">ng-bind</span>=<span class="string">"counter"</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color:blue"</span> <span class="attr">ng-bind</span>=<span class="string">"counter"</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color:green"</span> <span class="attr">ng-bind</span>=<span class="string">"counter"</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">"text/javascript"</span>&gt;</span></span><br><span class="line"><span class="actionscript">            <span class="comment">/* 数据模型区开始 */</span></span></span><br><span class="line"><span class="actionscript">            <span class="keyword">var</span> counter = <span class="number">0</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="actionscript">            <span class="function"><span class="keyword">function</span> <span class="title">inc</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line">                counter++;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line"><span class="actionscript">            <span class="function"><span class="keyword">function</span> <span class="title">reset</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line">                counter = 0;</span><br><span class="line">            &#125;</span><br><span class="line"><span class="actionscript">            <span class="comment">/* 数据模型区结束 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="actionscript">            <span class="comment">/* 绑定关系区开始 */</span></span></span><br><span class="line"><span class="actionscript">            <span class="function"><span class="keyword">function</span> <span class="title">init</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line">                bind();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line"><span class="actionscript">            <span class="function"><span class="keyword">function</span> <span class="title">bind</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line"><span class="javascript">                <span class="keyword">var</span> list = <span class="built_in">document</span>.querySelectorAll(<span class="string">"[ng-click]"</span>);</span></span><br><span class="line"><span class="actionscript">                <span class="keyword">for</span> (<span class="keyword">var</span> i=<span class="number">0</span>; i&lt;list.length; i++) &#123;</span></span><br><span class="line"><span class="actionscript">                    list[i].onclick = (<span class="function"><span class="keyword">function</span><span class="params">(index)</span> </span>&#123;</span></span><br><span class="line"><span class="actionscript">                        <span class="keyword">return</span> <span class="function"><span class="keyword">function</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line"><span class="javascript">                            <span class="built_in">window</span>[list[index].getAttribute(<span class="string">"ng-click"</span>)]();</span></span><br><span class="line">                            apply();</span><br><span class="line">                        &#125;;</span><br><span class="line">                    &#125;)(i);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line"><span class="actionscript">            <span class="function"><span class="keyword">function</span> <span class="title">apply</span><span class="params">()</span> </span>&#123;</span></span><br><span class="line"><span class="javascript">                <span class="keyword">var</span> list = <span class="built_in">document</span>.querySelectorAll(<span class="string">"[ng-bind='counter']"</span>);</span></span><br><span class="line"><span class="actionscript">                <span class="keyword">for</span> (<span class="keyword">var</span> i=<span class="number">0</span>; i&lt;list.length; i++) &#123;</span></span><br><span class="line">                    if (list[i].innerHTML != counter) &#123;</span><br><span class="line">                        list[i].innerHTML = counter;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"><span class="actionscript">            <span class="comment">/* 绑定关系区结束 */</span></span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这样做的坏处是自己变更数据后，是无法自动改变DOM的，必须要想办法触发<code>apply()</code>，所以只能借助<code>ng-click</code>的包装，在<code>ng-click</code>中包含真实的<code>click</code>事件监听并追加脏值检测以判断是否要更新DOM。  </p><p>另外一个坏处是如果不注意，每次脏值检测会检测大量的数据，而很多数据是没有检测的必要的，容易影响性能。  </p><p>关于如何实现一个和angular一样的脏值检测，知道原理后还有很多工作要去做，以及如何优化等等。如果有兴趣可以看看民工叔曾经推荐的《Build Your Own Angular.js》，第一章<code>Scope</code>便讲了如何实现angular的作用域和脏值检测。对了，上面的例子也是从民工叔的博客稍加修改来的，建议最后去看下原文，链接在参考资料中。</p><h2 id="ES5的getter与setter"><a href="#ES5的getter与setter" class="headerlink" title="ES5的getter与setter"></a>ES5的<code>getter</code>与<code>setter</code></h2><p>在ES5中新增了一个<code>Object.defineProperty</code>，直接在一个对象上定义一个新属性，或者修改一个已经存在的属性， 并返回这个对象。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Object</span>.defineProperty(obj, prop, descriptor)</span><br></pre></td></tr></table></figure><p>其接受的第三个参数可以取<code>get</code>和<code>set</code>并各自对应一个<code>getter</code>和<code>setter</code>方法：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a = &#123; <span class="attr">zhihu</span>:<span class="number">0</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.defineProperty(a, <span class="string">'zhihu'</span>, &#123;</span><br><span class="line">  <span class="keyword">get</span>: function() &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'get：'</span> + zhihu);</span><br><span class="line">    <span class="keyword">return</span> zhihu;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="keyword">set</span>: function(value) &#123;</span><br><span class="line">    zhihu = value;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'set:'</span> + zhihu);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">a.zhihu = <span class="number">2</span>; <span class="comment">// set:2</span></span><br><span class="line"><span class="built_in">console</span>.log(a.zhihu); <span class="comment">// get：2</span></span><br><span class="line">                      <span class="comment">// 2</span></span><br></pre></td></tr></table></figure><p>基于ES5的<code>getter</code>和<code>setter</code>可以说几乎完美符合了要求。为什么要说<code>几乎</code>呢？  </p><p>首先IE8及更低版本IE是无法使用的，而且这个特性是没有<code>polyfill</code>的，无法在不支持的平台实现，<br>这也是基于ES5<code>getter</code>和<code>setter</code>的Vue.js不支持IE8及更低版本IE的原因。也许有人会提到<code>avalon</code>，<code>avalon</code>在低版本IE借助<code>vbscript</code>一些黑魔法实现了类似的功能。  </p><p>除此之外，还有一个问题就是修改数组的<code>length</code>，直接用索引设置元素如<code>items[0] = {}</code>，以及数组的<code>push</code>等变异方法是无法触发<code>setter</code>的。<br>如果想要解决这个问题可以参考Vue的做法，在Vue的<code>observer/array.js</code>中，Vue直接修改了数组的原型方法：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arrayProto = <span class="built_in">Array</span>.prototype</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> arrayMethods = <span class="built_in">Object</span>.create(arrayProto)</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Intercept mutating methods and emit events</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line">;[</span><br><span class="line">  <span class="string">'push'</span>,</span><br><span class="line">  <span class="string">'pop'</span>,</span><br><span class="line">  <span class="string">'shift'</span>,</span><br><span class="line">  <span class="string">'unshift'</span>,</span><br><span class="line">  <span class="string">'splice'</span>,</span><br><span class="line">  <span class="string">'sort'</span>,</span><br><span class="line">  <span class="string">'reverse'</span></span><br><span class="line">]</span><br><span class="line">.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">method</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// cache original method</span></span><br><span class="line">  <span class="keyword">var</span> original = arrayProto[method]</span><br><span class="line">  def(arrayMethods, method, <span class="function"><span class="keyword">function</span> <span class="title">mutator</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="comment">// avoid leaking arguments:</span></span><br><span class="line">    <span class="comment">// http://jsperf.com/closure-with-arguments</span></span><br><span class="line">    <span class="keyword">var</span> i = <span class="built_in">arguments</span>.length</span><br><span class="line">    <span class="keyword">var</span> args = <span class="keyword">new</span> <span class="built_in">Array</span>(i)</span><br><span class="line">    <span class="keyword">while</span> (i--) &#123;</span><br><span class="line">      args[i] = <span class="built_in">arguments</span>[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> result = original.apply(<span class="keyword">this</span>, args)</span><br><span class="line">    <span class="keyword">var</span> ob = <span class="keyword">this</span>.__ob__</span><br><span class="line">    <span class="keyword">var</span> inserted</span><br><span class="line">    <span class="keyword">switch</span> (method) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">'push'</span>:</span><br><span class="line">        inserted = args</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      <span class="keyword">case</span> <span class="string">'unshift'</span>:</span><br><span class="line">        inserted = args</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      <span class="keyword">case</span> <span class="string">'splice'</span>:</span><br><span class="line">        inserted = args.slice(<span class="number">2</span>)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (inserted) ob.observeArray(inserted)</span><br><span class="line">    <span class="comment">// notify change</span></span><br><span class="line">    ob.dep.notify()</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这样重写了原型方法，在执行数组变异方法后依然能够触发视图的更新。  </p><p>但是这样还是不能解决修改数组的<code>length</code>和直接用索引设置元素如<code>items[0] = {}</code>的问题，想要解决依然可以参考Vue的做法：<br>前一个问题可以直接用新的数组代替旧的数组；后一个问题可以为数组拓展一个<code>$set</code>方法，在执行修改后顺便触发视图的更新。</p><h2 id="已被废弃的Object-observe"><a href="#已被废弃的Object-observe" class="headerlink" title="已被废弃的Object.observe"></a>已被废弃的<code>Object.observe</code></h2><p><code>Object.observe</code>曾在ES7的草案中，并在提议中进展到stage2，最终依然被废弃。<br>这里只举一个MDN上的例子：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一个数据模型</span></span><br><span class="line"><span class="keyword">var</span> user = &#123;</span><br><span class="line">  id: <span class="number">0</span>,</span><br><span class="line">  name: <span class="string">'Brendan Eich'</span>,</span><br><span class="line">  title: <span class="string">'Mr.'</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建用户的greeting</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">updateGreeting</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  user.greeting = <span class="string">'Hello, '</span> + user.title + <span class="string">' '</span> + user.name + <span class="string">'!'</span>;</span><br><span class="line">&#125;</span><br><span class="line">updateGreeting();</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.observe(user, <span class="function"><span class="keyword">function</span>(<span class="params">changes</span>) </span>&#123;</span><br><span class="line">  changes.forEach(<span class="function"><span class="keyword">function</span>(<span class="params">change</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 当name或title属性改变时, 更新greeting</span></span><br><span class="line">    <span class="keyword">if</span> (change.name === <span class="string">'name'</span> || change.name === <span class="string">'title'</span>) &#123;</span><br><span class="line">      updateGreeting();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>由于是已经废弃了的特性，Chrome虽然曾经支持但也已经废弃了支持，这里不再讲更多，有兴趣可以搜一搜以前的文章，这曾经是一个被看好的特性（<a href="http://div.io/topic/600" target="_blank" rel="noopener">Object.observe()带来的数据绑定变革</a>）。<br>当然关于它也有一些替代品<a href="https://github.com/polymer/observe-js" target="_blank" rel="noopener">Polymer/observe-js</a>。</p><h2 id="ES6带来的Proxy"><a href="#ES6带来的Proxy" class="headerlink" title="ES6带来的Proxy"></a>ES6带来的<code>Proxy</code></h2><p>人如其名，类似HTTP中的代理：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> p = <span class="keyword">new</span> <span class="built_in">Proxy</span>(target, handler);</span><br></pre></td></tr></table></figure><p><code>target</code>为目标对象，可以是任意类型的对象，比如数组，函数，甚至是另外一个代理对象。<br><code>handler</code>为处理器对象，包含了一组代理方法，分别控制所生成代理对象的各种行为。</p><p>举个例子：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = <span class="keyword">new</span> <span class="built_in">Proxy</span>(&#123;&#125;, &#123;</span><br><span class="line">  <span class="keyword">set</span>: function(obj, prop, value) &#123;</span><br><span class="line">    obj[prop] = value;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (prop === <span class="string">'zhihu'</span>) &#123;</span><br><span class="line">      <span class="built_in">console</span>.log(<span class="string">"set "</span> + prop + <span class="string">": "</span> + obj[prop]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">a.zhihu = <span class="number">100</span>;</span><br></pre></td></tr></table></figure><p>当然，<code>Proxy</code>的能力远不止此，还可以实现代理转发等等。  </p><p>但是要注意的是目前浏览器中只有Firefox18支持这个特性，而babel官方也表明不支持这个特性：</p><blockquote><p>Unsupported feature<br>Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled.</p></blockquote><p>目前已经有babel插件可以实现，但是据说实现的比较复杂。<br>如果是Node的话升级到目前的最新版本应该就可以使用了，上面的例子测试环境为Node v6.4.0。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://github.com/xufei/blog/issues/10" target="_blank" rel="noopener">Angular沉思录（一）数据绑定</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" target="_blank" rel="noopener">Object.defineProperty() - JavaScript | MDN</a></li><li><a href="https://github.com/vuejs/vue/blob/dev/src/observer/array.js" target="_blank" rel="noopener">vue/array.js at dev · vuejs/vue</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/observe" target="_blank" rel="noopener">Object.observe() - JavaScript | MDN</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy" target="_blank" rel="noopener">Proxy - JavaScript | MDN</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/44724640&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;如何监听 js 中变量的变化?&lt;/a&gt;&lt;br&gt;我现在有这样一个需求，需要监控js的某个变量的改变，如果该变量发生变化，则触发一些事件，不能使用timeinterval之类的定时去监控的方法，不知道有比较好的解决方案么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题问的很好。  &lt;/p&gt;
&lt;p&gt;流行的MVVM的JS库/框架都有共同的特点就是数据绑定，在数据变更后响应式的自动进行相关计算并变更DOM展现。所以这个问题也可以理解为&lt;strong&gt;如何实现MVVM库/框架的数据绑定&lt;/strong&gt;。  &lt;/p&gt;
&lt;p&gt;常见的数据绑定的实现有脏值检测，基于ES5的&lt;code&gt;getter&lt;/code&gt;和&lt;code&gt;setter&lt;/code&gt;，以及ES已被废弃的&lt;code&gt;Object.observe&lt;/code&gt;，和ES6中添加的&lt;code&gt;Proxy&lt;/code&gt;。&lt;/p&gt;
    
    </summary>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/tags/JavaScript/"/>
    
      <category term="Proxy" scheme="https://blog.daraw.cn/tags/Proxy/"/>
    
      <category term="脏值检测" scheme="https://blog.daraw.cn/tags/%E8%84%8F%E5%80%BC%E6%A3%80%E6%B5%8B/"/>
    
  </entry>
  
  <entry>
    <title>JS实现自定义事件</title>
    <link href="https://blog.daraw.cn/2016/08/02/javascript-event-emitter/"/>
    <id>https://blog.daraw.cn/2016/08/02/javascript-event-emitter/</id>
    <published>2016-08-02T22:30:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="要求"><a href="#要求" class="headerlink" title="要求"></a>要求</h2><blockquote><p>请实现下面的自定义事件Event对象的接口，功能见注释（测试1）<br>该Event对象的接口需要能被其他对象拓展复用（测试2）</p></blockquote><a id="more"></a><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 测试 1</span></span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result);</span><br><span class="line">&#125;)</span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"test"</span>);</span><br><span class="line">&#125;)</span><br><span class="line">Event.emit(<span class="string">"test"</span>, <span class="string">"hello world"</span>); <span class="comment">// 输出“hello world”和”test”</span></span><br><span class="line"><span class="comment">//测试2</span></span><br><span class="line"><span class="keyword">var</span> person1 = &#123;&#125;;</span><br><span class="line"><span class="keyword">var</span> person2 = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.assign(person1, Event);</span><br><span class="line"><span class="built_in">Object</span>.assign(person2, Event);</span><br><span class="line"></span><br><span class="line">person1.on(<span class="string">"call1"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person1"</span>);</span><br><span class="line">&#125;);</span><br><span class="line">person2.on(<span class="string">"call2"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person2"</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">person1.emit(<span class="string">"call1"</span>); <span class="comment">// 输出“person1”</span></span><br><span class="line">person1.emit(<span class="string">"call2"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call1"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call2"</span>); <span class="comment">// 输出”person2”</span></span><br></pre></td></tr></table></figure><h2 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h2><p>这是个经典的自定义事件，实现”Pub/Sub”，Node.js中有个比较完善的实现<code>EventEmitter</code>。  </p><p>原理则是构造出一个集成队列的对象，每一个事件对应对象的一个队列，在自定义事件后将回调函数入队，触发事件后回调函数依次出队并执行。  </p><p>个人偏爱IIFE+构造器模式+原型模式，于是有了下面的一个简易实现：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">(<span class="function"><span class="keyword">function</span> (<span class="params">exports</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">EventEmitter</span>(<span class="params"></span>) </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">  EventEmitter.prototype.on = <span class="function"><span class="keyword">function</span> (<span class="params">type, handler</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue = <span class="keyword">this</span>._cbQueue || &#123;&#125;;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type] = <span class="keyword">this</span>._cbQueue[type]|| [];</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type].push(handler);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  EventEmitter.prototype.emit = <span class="function"><span class="keyword">function</span> (<span class="params">type, data</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>._cbQueue[type]) &#123;</span><br><span class="line">      <span class="keyword">this</span>._cbQueue[type].forEach(<span class="function"><span class="keyword">function</span> (<span class="params">cb</span>) </span>&#123;</span><br><span class="line">        cb(data);</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  exports.EventEmitter = EventEmitter;</span><br><span class="line"></span><br><span class="line">&#125;(global));</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> Event = <span class="keyword">new</span> EventEmitter();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Test 1</span></span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result);</span><br><span class="line">&#125;)</span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"test"</span>);</span><br><span class="line">&#125;)</span><br><span class="line">Event.emit(<span class="string">"test"</span>, <span class="string">"hello world"</span>); <span class="comment">// 输出“hello world”和”test”</span></span><br></pre></td></tr></table></figure><p>这样便满足了测试1的要求，然而这样对于测试2是不行的，因为<code>Object.assign()</code>方法不能复制不可遍历的属性和继承属性，也就意味着<code>Event</code>对象上的<code>on</code>和<code>emit</code>不能被复制过去。那么复制<code>Event.prototype</code>对象呢？这样确实是可行的：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//Test2</span></span><br><span class="line"><span class="keyword">var</span> person1 = &#123;&#125;;</span><br><span class="line"><span class="keyword">var</span> person2 = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.assign(person1, EventEmitter.prototype);</span><br><span class="line"><span class="built_in">Object</span>.assign(person2, EventEmitter.prototype);</span><br><span class="line"></span><br><span class="line">person1.on(<span class="string">"call1"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person1"</span>);</span><br><span class="line">&#125;);</span><br><span class="line">person2.on(<span class="string">"call2"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person2"</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">person1.emit(<span class="string">"call1"</span>); <span class="comment">// 输出“person1”</span></span><br><span class="line">person1.emit(<span class="string">"call2"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call1"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call2"</span>); <span class="comment">// 输出”person2”</span></span><br></pre></td></tr></table></figure><p>虽然差不多实现了题目中的要求，但是和题目中的要求依然有些偏差，于是我又想到了，如果放弃原型模式+构造器模式，单纯的把Event作为一个对象而不是一个函数呢？<br>实现很简单：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line">(<span class="function"><span class="keyword">function</span> (<span class="params">exports</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> Event = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  Event.on = <span class="function"><span class="keyword">function</span> (<span class="params">type, handler</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue = <span class="keyword">this</span>._cbQueue || &#123;&#125;;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type] = <span class="keyword">this</span>._cbQueue[type]|| [];</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type].push(handler);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  Event.emit = <span class="function"><span class="keyword">function</span> (<span class="params">type, data</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>._cbQueue[type]) &#123;</span><br><span class="line">      <span class="keyword">this</span>._cbQueue[type].forEach(<span class="function"><span class="keyword">function</span> (<span class="params">cb</span>) </span>&#123;</span><br><span class="line">        cb(data);</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  exports.Event = Event;</span><br><span class="line"></span><br><span class="line">&#125;(global));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// Test 1</span></span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result);</span><br><span class="line">&#125;)</span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"test"</span>);</span><br><span class="line">&#125;)</span><br><span class="line">Event.emit(<span class="string">"test"</span>, <span class="string">"hello world"</span>); <span class="comment">// 输出“hello world”和”test”</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//Test2</span></span><br><span class="line"><span class="keyword">var</span> person1 = &#123;&#125;;</span><br><span class="line"><span class="keyword">var</span> person2 = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.assign(person1, Event);</span><br><span class="line"><span class="built_in">Object</span>.assign(person2, Event);</span><br><span class="line"></span><br><span class="line">person1.on(<span class="string">"call1"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person1"</span>);</span><br><span class="line">&#125;);</span><br><span class="line">person2.on(<span class="string">"call2"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person2"</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">person1.emit(<span class="string">"call1"</span>); <span class="comment">// 输出“person1”</span></span><br><span class="line">person1.emit(<span class="string">"call2"</span>); <span class="comment">// 输出”person2”</span></span><br><span class="line">person2.emit(<span class="string">"call1"</span>); <span class="comment">// 输出”person1”</span></span><br><span class="line">person2.emit(<span class="string">"call2"</span>); <span class="comment">// 输出”person2”</span></span><br></pre></td></tr></table></figure><p>测试1的结果如预期一样，然而测试2却出了问题，debug看了一下<code>Event</code>对象，<code>Event.on</code>执行后上面竟然有个可以遍历的属性<code>_cbQueue</code>，而且<code>_cbQueue</code>是一个对象而不是一个字面量，所以在<code>Object.assign()</code>拷贝的过程中，将<code>Event._cbQueue</code>对象引用赋值给了<code>person1._cbQueue</code>和<code>person2._cbQueue</code>，也就是说这三者指向了内存中的同一个对象，只要修改一个，其他几个都会跟着修改；当不执行<code>Event.on</code>时，<code>Event</code>上就不会有属性<code>_cbQueue</code>，那么接下来<code>person1</code>和<code>person2</code>执行<code>on</code>方法后，<code>this</code>指向了他们本身，会创造他们自己的互不影响的<code>_cbQueue</code>属性。<br>但不执行<code>Event.on</code>这不是解决的完美办法，不过问题已经定位了，解决也很简单，修改<code>Event</code>对象的属性<code>_cbQueue</code>为不可遍历，让拷贝过程不拷贝属性<code>_cbQueue</code>即可：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">(<span class="function"><span class="keyword">function</span> (<span class="params">exports</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> Event = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">Object</span>.defineProperty(Event, <span class="string">"_cbQueue"</span>, &#123;</span><br><span class="line">  value : &#123;&#125;,</span><br><span class="line">  writable : <span class="literal">true</span>,</span><br><span class="line">  enumerable : <span class="literal">false</span>,</span><br><span class="line">  configurable : <span class="literal">true</span></span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  Event.on = <span class="function"><span class="keyword">function</span> (<span class="params">type, handler</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue = <span class="keyword">this</span>._cbQueue || &#123;&#125;;</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type] = <span class="keyword">this</span>._cbQueue[type]|| [];</span><br><span class="line">    <span class="keyword">this</span>._cbQueue[type].push(handler);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  Event.emit = <span class="function"><span class="keyword">function</span> (<span class="params">type, data</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>._cbQueue[type]) &#123;</span><br><span class="line">      <span class="keyword">this</span>._cbQueue[type].forEach(<span class="function"><span class="keyword">function</span> (<span class="params">cb</span>) </span>&#123;</span><br><span class="line">        cb(data);</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  exports.Event = Event;</span><br><span class="line"></span><br><span class="line">&#125;(global));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// Test 1</span></span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(result);</span><br><span class="line">&#125;)</span><br><span class="line">Event.on(<span class="string">"test"</span>, <span class="function"><span class="keyword">function</span>(<span class="params">result</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"test"</span>);</span><br><span class="line">&#125;)</span><br><span class="line">Event.emit(<span class="string">"test"</span>, <span class="string">"hello world"</span>); <span class="comment">// 输出“hello world”和”test”</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//Test2</span></span><br><span class="line"><span class="keyword">var</span> person1 = &#123;&#125;;</span><br><span class="line"><span class="keyword">var</span> person2 = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Object</span>.assign(person1, Event);</span><br><span class="line"><span class="built_in">Object</span>.assign(person2, Event);</span><br><span class="line"></span><br><span class="line">person1.on(<span class="string">"call1"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person1"</span>);</span><br><span class="line">&#125;);</span><br><span class="line">person2.on(<span class="string">"call2"</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">"person2"</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">person1.emit(<span class="string">"call1"</span>); <span class="comment">// 输出“person1”</span></span><br><span class="line">person1.emit(<span class="string">"call2"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call1"</span>); <span class="comment">// 没有输出</span></span><br><span class="line">person2.emit(<span class="string">"call2"</span>); <span class="comment">// 输出”person2”</span></span><br></pre></td></tr></table></figure><p>这样便算完美实现了题目的要求。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>其实原理很简单，没想到在周边的实现上浪费了一些debug的时间，有些惭愧。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;要求&quot;&gt;&lt;a href=&quot;#要求&quot; class=&quot;headerlink&quot; title=&quot;要求&quot;&gt;&lt;/a&gt;要求&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;请实现下面的自定义事件Event对象的接口，功能见注释（测试1）&lt;br&gt;该Event对象的接口需要能被其他对象拓展复用（测试2）&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>制作列带有斑马条纹背景的表格</title>
    <link href="https://blog.daraw.cn/2016/05/16/table-with-striped-lines/"/>
    <id>https://blog.daraw.cn/2016/05/16/table-with-striped-lines/</id>
    <published>2016-05-16T13:27:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>在切页面时，遇到了一个有趣的表格，如图所示：</p><a id="more"></a><p><img src="/img/table-with-striped-lines0.png" alt="">  </p><p>下意识的想到了<code>bootstrap</code>的斑马纹效果table，然而记忆中bs只有行斑马纹效果，至于实现事实上很简单，直接去Github看最终生成的<code>bootstrap.css</code>文件，当前的2321~2323行为：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.table-striped</span> &gt; <span class="selector-tag">tbody</span> &gt; <span class="selector-tag">tr</span><span class="selector-pseudo">:nth-of-type(odd)</span> &#123;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#f9f9f9</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>事实上这给了我一个很好的思路，借助<code>nth-child()</code>选择器，先对每行的奇数<code>td</code>设定背景，连起来就是奇数列做了特殊背景处理，也就是斑马纹效果，然后再对第一行和最后一行做特殊处理让边角圆润，下面贴上自己的代码（主要提供一个思路，css比较烂，请轻喷==）：<br><strong>html</strong></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">class</span>=<span class="string">"account-course-lessons"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tbody</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">"checkbox"</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>L1<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">"checkbox"</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>L2<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">"checkbox"</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>L3<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">"checkbox"</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>L4<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">"checkbox"</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>L5<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">tr</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">td</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">tbody</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>scss</strong>  </p><figure class="highlight scss"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">table</span><span class="selector-class">.account-course-lessons</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">600px</span>;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">300px</span>;</span><br><span class="line">  <span class="attribute">margin</span>: <span class="number">1em</span>;</span><br><span class="line">  <span class="attribute">background-color</span>: rgb(<span class="number">221</span>, <span class="number">221</span>, <span class="number">221</span>);</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">10px</span>;</span><br><span class="line">  <span class="attribute">text-align</span>: center;</span><br><span class="line">  <span class="attribute">text-decoration</span>: underline;</span><br><span class="line"></span><br><span class="line">  <span class="selector-tag">tr</span> &#123;</span><br><span class="line">    <span class="selector-tag">td</span> &#123;</span><br><span class="line">      <span class="attribute">border-bottom</span>: <span class="number">2px</span> solid rgb(<span class="number">215</span>, <span class="number">215</span>, <span class="number">215</span>);</span><br><span class="line">      <span class="attribute">height</span>: <span class="number">60px</span>;</span><br><span class="line"></span><br><span class="line">      <span class="selector-tag">a</span> &#123;</span><br><span class="line">        <span class="attribute">color</span>: gray;</span><br><span class="line">        <span class="attribute">font-size</span>: <span class="number">1.5em</span>;</span><br><span class="line">        <span class="attribute">margin</span>: <span class="number">0</span> <span class="number">1em</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-tag">tr</span> &#123;</span><br><span class="line">    <span class="selector-tag">td</span><span class="selector-pseudo">:nth-child</span>(2n+1) &#123;</span><br><span class="line">      <span class="attribute">background-color</span>: rgb(<span class="number">235</span>, <span class="number">235</span>, <span class="number">235</span>);</span><br><span class="line">      <span class="attribute">border-bottom</span>: <span class="number">2px</span> solid rgb(<span class="number">215</span>, <span class="number">215</span>, <span class="number">215</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-tag">tr</span><span class="selector-pseudo">:nth-child</span>(1) &#123;</span><br><span class="line">    <span class="selector-tag">td</span><span class="selector-pseudo">:nth-child</span>(2n+1) &#123;</span><br><span class="line">      <span class="attribute">background-color</span>: rgb(<span class="number">235</span>, <span class="number">235</span>, <span class="number">235</span>);</span><br><span class="line">      <span class="attribute">border-top</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-left</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-right</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-radius</span>: <span class="number">20px</span> <span class="number">20px</span> <span class="number">0</span> <span class="number">0</span> ;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-tag">tr</span><span class="selector-pseudo">:nth-last-child</span>(1) &#123;</span><br><span class="line">    <span class="selector-tag">td</span><span class="selector-pseudo">:nth-child</span>(2n+1) &#123;</span><br><span class="line">      <span class="attribute">background-color</span>: rgb(<span class="number">235</span>, <span class="number">235</span>, <span class="number">235</span>);</span><br><span class="line">      <span class="attribute">border-bottom</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-left</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-right</span>: <span class="number">10px</span> solid transparent;</span><br><span class="line">      <span class="attribute">border-radius</span>:  <span class="number">0</span> <span class="number">0</span> <span class="number">20px</span> <span class="number">20px</span> ;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="selector-tag">td</span> &#123;</span><br><span class="line">      <span class="attribute">border-bottom</span>: <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在切页面时，遇到了一个有趣的表格，如图所示：&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="css" scheme="https://blog.daraw.cn/tags/css/"/>
    
  </entry>
  
  <entry>
    <title>git用户名莫名其妙变化及挽救措施</title>
    <link href="https://blog.daraw.cn/2016/04/22/git-commit-name-change/"/>
    <id>https://blog.daraw.cn/2016/04/22/git-commit-name-change/</id>
    <published>2016-04-22T09:59:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<p>昨晚提交了一次commit并推到github的远程仓库后，突然发现最近的commit提交都没有记录到contributions里，而且commit记录中我的头像无法正常显示。  </p><a id="more"></a><p>查了一下查到了github的帮助文档：</p><ul><li><a href="https://help.github.com/articles/why-are-my-contributions-not-showing-up-on-my-profile/" target="_blank" rel="noopener">Why are my contributions not showing up on my profile?</a></li><li><a href="https://help.github.com/articles/changing-author-info/" target="_blank" rel="noopener">Changing author info</a></li></ul><p>英语不好的还可以看看这篇博客：<a href="https://segmentfault.com/a/1190000004318632" target="_blank" rel="noopener">为什么Github没有记录你的Contributions</a>  </p><p>在本地仓库目录下<code>git log</code>查看了历史记录，发现出问题的那几次的用户名变成了windows的用户名，邮箱也是用户名。<br>按照帮助文档修复了commit作者信息，并提交了github的远程仓库后，终于正常了。  </p><p>最后，在git bash中设定好git配置文件的用户信息：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global user.name "CodeDaraW"</span><br><span class="line">git config --global user.email "CodeDaraW@gmail.com"</span><br></pre></td></tr></table></figure><p>一切就正常了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;昨晚提交了一次commit并推到github的远程仓库后，突然发现最近的commit提交都没有记录到contributions里，而且commit记录中我的头像无法正常显示。  &lt;/p&gt;
    
    </summary>
    
    
      <category term="git" scheme="https://blog.daraw.cn/categories/git/"/>
    
    
      <category term="git" scheme="https://blog.daraw.cn/tags/git/"/>
    
  </entry>
  
  <entry>
    <title>记一次前端性能优化实战</title>
    <link href="https://blog.daraw.cn/2016/03/03/note-for-optimization-of-online-shop/"/>
    <id>https://blog.daraw.cn/2016/03/03/note-for-optimization-of-online-shop/</id>
    <published>2016-03-03T00:00:00.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这学期开学时去找了信息化中心的老师，说明了自己想往Node和Python方向走的想法，退出了信息化中心的学生工作室。<br>然而上周六老师又找到了我，他们对一个电商网站项目的前端性能很不满意，希望我能给他们做一套性能优化方案。</p><a id="more"></a><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>和老师面谈后，确定了需要下面几个主要需求：  </p><ul><li>合并css/js文件减少http请求</li><li>压缩css/js文件减少流量</li><li>原来的模板中每个小icon都是一张图片，用雪碧图/base64减少http请求</li><li>大图懒加载，减轻后端压力</li></ul><h2 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h2><p>常见的构建方案就是grunt,gulp,webpack,make,npm script，还有国内百度的fis。<br>经过了筛选省下了gulp和fis3两个选择，不得不说fis3已经做好了一套默认方案，几乎和信息中心的要求相符，然而经过测试发现这个项目使用gulp构建更加快一些。<br>确定了gulp后，接下来就是对照需求选插件了。</p><ul><li>开发时实时监听变化自动刷新<br>借助<code>gulp-livereload</code>插件和chrome的<code>livereload</code>插件，<code>gulpfile</code>中创建名为<code>watch</code>的<code>task</code>，监听文件变动浏览器自动刷新，解放浏览器的刷新按钮和键盘的F5。</li><li>合并js/css文件<br>为了方便开发时多个文件引入，和模块化的开发，一开始使用了<code>gulp-usemin</code>插件，使用起来确实挺方便的，然而发现在多个页面构建时就废了，查了一下发现npm建议使用<code>gulp-useref</code>代替。<br>然而接着发现<code>gulp-useref</code>不能将外联转化为内联，而且在流中处理了文件名后不会像<code>gulp-usemin</code>自动修改引用文件的地址，好在有<code>gulp-rev-replace</code>插件可以自动在流中修改html文件中的引用地址。<br>而将外联文件合并为内联文件则可以用<code>gulp-inline-source</code>插件，和<code>gulp-usemin</code>差不多。</li><li>js/css文件压缩<br>分别用<code>gulp-uglify</code>和<code>gulp-cssnano</code>插件，这个没什么好说的。</li><li>版本更迭css/js文件冗余<br>常见的有文件名加时间戳和hash两种方案，选哪个倒感觉无所谓了，这边用<code>gulp-rev</code>插件实现hash冗余文件，服务器端静态资源半年清理个一次也就差不多了。</li><li>小图优化<br>常见的就是把小图转成字体文件，SVG，雪碧图，base64这几种方案。<br>这边选择了base64，其实base64有利有害，合并进css文件会变大1/3左右，而且css文件太大后会延长css文件渲染时间。这边我设定的5KB为阈值，css文件大概增加了100k，影响不大，图片跟着css文件一起缓存，倒也不错。</li><li>大图懒加载<br>由于原来就使用了jquery，于是用了百度改过的jquery图片懒加载插件，这里遇到了一个大坑。<br>假设<code>html</code>中有两个<code>div</code>，通过css将第一块放在左边当作侧栏，第二块放在右边当作主栏，将所有图片都做懒加载处理，如果左栏高度过高，左栏看完了右栏才能开始加载。<br>鹅厂的大牛张鑫旭曾经也写过一个这样的插件。这类的插件原理都类似，大致看了下源码，猜测是插件设计时没有想到这种场景，所以出现了这种蜜汁bug。<br>突然想起来，百度说他的百度地图和糯米都用了这个插件，那不如去看看他们怎么解决的。<br>看了下糯米的页面，原来他们不对侧栏做懒加载处理。我取消了侧栏的懒加载处理，果然就正常了。</li></ul><h2 id="未完待续"><a href="#未完待续" class="headerlink" title="未完待续"></a>未完待续</h2><p>这是第一次将这些优化方案在这样一个较大的项目中使用，这个项目还没有结束，如果有新的变化视情况来更新。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;这学期开学时去找了信息化中心的老师，说明了自己想往Node和Python方向走的想法，退出了信息化中心的学生工作室。&lt;br&gt;然而上周六老师又找到了我，他们对一个电商网站项目的前端性能很不满意，希望我能给他们做一套性能优化方案。&lt;/p&gt;
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.daraw.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="gulp" scheme="https://blog.daraw.cn/tags/gulp/"/>
    
      <category term="性能优化" scheme="https://blog.daraw.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>将setTimeout函数队列</title>
    <link href="https://blog.daraw.cn/2016/02/12/queue-several-functions-set-by-timeout/"/>
    <id>https://blog.daraw.cn/2016/02/12/queue-several-functions-set-by-timeout/</id>
    <published>2016-02-12T14:36:41.000Z</published>
    <updated>2026-03-28T15:41:48.518Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>先来看一段代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'3000ms'</span>);</span><br><span class="line">&#125;, <span class="number">3000</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'first'</span>);</span><br><span class="line"></span><br><span class="line">setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'1000ms'</span>);</span><br><span class="line">&#125;, <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'second'</span>);</span><br></pre></td></tr></table></figure><p>控制台输出为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">first</span><br><span class="line">second</span><br><span class="line">1000ms</span><br><span class="line">3000ms</span><br></pre></td></tr></table></figure><p>如果我们想让第一个定时器的回调函数执行完再执行第二个定时器的回调函数该怎么做呢？</p><a id="more"></a><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>JavaScript一开始作为浏览器脚本语言出现，为了避免多线程带来的管理问题，被设计成了单线程，即使是Node.js中的<code>cluster</code>以及Web Worker也并没有改变JS单线程的本质。<br>单线程意味着阻塞，所有的任务要排队执行，前一个任务未执行完下一个任务是不会执行的。如果有一个任务耗时很长，那么后面的任务也只有干等着。为了解决这个问题，JS内部还有一个消息队列，并采用EventLoop来处理消息队列中的消息。<br>在上述栗子中，<code>setTimeout</code>会在指定的时间向消息队列中添加一条消息，在消息得到处理的时候也就是回调函数执行的时候，而主线程中的<code>console.log</code>不会等待，所以上述栗子中先按顺序执行了两个<code>console.log</code>，然后才按照定时分别执行1000ms和3000ms时的回调函数。<br>关于JS的单线程的问题这里不做更多解释，想了解更多的话可以看看下面参考中的阮一峰老师的<a href="http://javascript.ruanyifeng.com/bom/engine.html" target="_blank" rel="noopener">浏览器的JavaScript引擎</a>，讲的非常易懂。  </p><p>那么上述问题中的需求就是说，如何做到在第一个定时器的回调函数执行后，第二个定时器才开始定时并等待执行回调函数。<br>最容易想到了方法就是回调函数。封装定时器，在回调中执行下一个封装函数，但这会导致一个问题，在稍微复杂的情况下，出现“回调地狱”，相信Node.js党应该对回调有着别样的感情。<br>So，回调函数并不是一个cool的解决方案。  </p><p>这里的另一个思路就是再构造一个函数队列，封装定时器，给定时器加锁，将其锁在一个函数里，只有确认执行结束才会执行队列中的下一个封装的定时器。  </p><h2 id="方案"><a href="#方案" class="headerlink" title="方案"></a>方案</h2><p>在StackOverflow上有个哥们提了个和我一样的问题，其中一个答案给了一个demo，我这里简化了一下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveOne</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveOne(arg1);&#125;, <span class="number">3000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func1 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveTwo</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line"><span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveTwo(arg1);&#125;, <span class="number">2000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func2 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveThree</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveThree(arg1);&#125;, <span class="number">1000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func3 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> funcSet = [recursiveOne, recursiveTwo, recursiveThree];</span><br><span class="line"><span class="keyword">var</span> funcArgs = [[<span class="literal">true</span>], [<span class="literal">true</span>], [<span class="literal">true</span>]];</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">coreFunction</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(funcSet.length)&#123;</span><br><span class="line">        <span class="keyword">var</span> func = funcSet.shift();</span><br><span class="line">        func.apply(global, funcArgs.shift())</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">coreFunction();</span><br></pre></td></tr></table></figure><p>免去了层层回调，优雅的解决的问题：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">fun1 complete</span><br><span class="line">fun2 complete</span><br><span class="line">fun3 complete</span><br></pre></td></tr></table></figure><p>那么在实际开发环境如何可控的操作执行队列和锁呢？<br>构造一个入队函数，和出队执行函数：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> funcSet = [],</span><br><span class="line">    funcArgs = [],</span><br><span class="line">    funcCont = [],</span><br><span class="line">    isRunning = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">enQueue</span>(<span class="params">fn, context, args</span>) </span>&#123;</span><br><span class="line">    funcSet.push(fn);</span><br><span class="line">    funcCont.push(context);</span><br><span class="line">    funcArgs.push(args);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!isRunning) &#123;</span><br><span class="line">        isRunning = <span class="literal">true</span>;</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">coreFunction</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(funcSet.length)&#123;</span><br><span class="line">        funcSet.shift().apply(funcCont.shift(), [].concat(funcArgs.shift()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整的代码实现如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveOne</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveOne(arg1);&#125;, <span class="number">3000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func1 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveTwo</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveTwo(arg1);&#125;, <span class="number">2000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func2 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">recursiveThree</span>(<span class="params">arg1</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(arg1)&#123;</span><br><span class="line">        arg1 = <span class="literal">false</span>;</span><br><span class="line">        setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;recursiveThree(arg1);&#125;, <span class="number">1000</span>);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">"func3 complete"</span>);</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> funcSet = [],</span><br><span class="line">    funcArgs = [],</span><br><span class="line">    funcCont = [],</span><br><span class="line">    isRunning = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">enQueue</span>(<span class="params">fn, context, args</span>) </span>&#123;</span><br><span class="line">    funcSet.push(fn);</span><br><span class="line">    funcCont.push(context);</span><br><span class="line">    funcArgs.push(args);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!isRunning) &#123;</span><br><span class="line">        isRunning = <span class="literal">true</span>;</span><br><span class="line">        coreFunction();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">coreFunction</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line"><span class="keyword">if</span>(funcSet.length)&#123;</span><br><span class="line">        funcSet.shift().apply(funcCont.shift(), [].concat(funcArgs.shift()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">enQueue(recursiveOne, <span class="keyword">this</span>, <span class="literal">true</span>);</span><br><span class="line">enQueue(recursiveTwo, <span class="keyword">this</span>, <span class="literal">true</span>);</span><br><span class="line">enQueue(recursiveThree, <span class="keyword">this</span>, <span class="literal">true</span>);</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://javascript.ruanyifeng.com/bom/engine.html" target="_blank" rel="noopener">浏览器的JavaScript引擎</a>  </li><li><a href="http://stackoverflow.com/questions/12839488/how-to-queue-several-functions-set-by-settimeout-with-js-jquery" target="_blank" rel="noopener">How to queue several functions set by setTimeout with JS/jQuery</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot; title=&quot;问题&quot;&gt;&lt;/a&gt;问题&lt;/h2&gt;&lt;p&gt;先来看一段代码：&lt;/p&gt;
&lt;figure class=&quot;highlight javascript&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;setTimeout(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;console&lt;/span&gt;.log(&lt;span class=&quot;string&quot;&gt;&#39;3000ms&#39;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;, &lt;span class=&quot;number&quot;&gt;3000&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;built_in&quot;&gt;console&lt;/span&gt;.log(&lt;span class=&quot;string&quot;&gt;&#39;first&#39;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;setTimeout(&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;&lt;/span&gt;) &lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;console&lt;/span&gt;.log(&lt;span class=&quot;string&quot;&gt;&#39;1000ms&#39;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;, &lt;span class=&quot;number&quot;&gt;1000&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;built_in&quot;&gt;console&lt;/span&gt;.log(&lt;span class=&quot;string&quot;&gt;&#39;second&#39;&lt;/span&gt;);&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;控制台输出为：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;first&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;second&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;1000ms&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3000ms&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;如果我们想让第一个定时器的回调函数执行完再执行第二个定时器的回调函数该怎么做呢？&lt;/p&gt;
    
    </summary>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.daraw.cn/tags/JavaScript/"/>
    
  </entry>
  
</feed>
