<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title></title>
        <link>undefined</link>
        <description>undefined</description>
        <lastBuildDate>Mon, 27 Dec 2021 10:52:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Joplin Pages Publisher</generator>
        <item>
            <title><![CDATA[控制流抽象]]></title>
            <guid>ab3b49dd35024bbd8b77c60f420c1e93</guid>
            <pubDate>Tue, 14 Dec 2021 12:45:03 GMT</pubDate>
            <content:encoded><![CDATA[<h2 id="控制流抽象与异步时代">控制流抽象与异步时代</h2>
<p>自结构化编程语言流行以来，编程语言仅允许程序员以<a>有限的几种方式调度控制流</a>。然而，控制流作为编程语言中的核心概念，对控制流进行抽象以更好地进行调度，是有必要的。</p>
<h3 id="异步编程">异步编程</h3>
<p><a title="https://www.zhihu.com/question/389262477/answer/1567044701" href="https://www.zhihu.com/question/389262477/answer/1567044701">随着互联网应用的发展</a>，应用层越来越多地使用异步调用（指调用方不等待返回值的调用。常见于 I/O 中）。其实对于软件的业务逻辑来说，本不存在“异步”的语义，<strong>程序员只是<a title="https://www.zhihu.com/question/46499998/answer/101672660" href="https://www.zhihu.com/question/46499998/answer/101672660">出于实现上的好处</a>（尤其是其对并发的支持）而不得不使用它</strong>。</p>
<p>异步的大量使用，使得程序员们需要发明抽象来辅助进行异步编程（<a title="https://www.zhihu.com/question/23886967/answer/145214574" href="https://www.zhihu.com/question/23886967/answer/145214574">为什么？</a>）。这条路上有两个路线：</p>
<ul>
<li>对异步产生的值进行抽象（例如<a>Promise</a>、<a>observable</a>等）</li>
<li><strong>对控制流本身进行抽象</strong></li>
</ul>
<p>这两条路线并不互斥，甚至有所重叠。本文将着重介绍第二条。</p>
<h3 id="continuation">continuation</h3>
<p>continuation 是一种语言级别的抽象，表示“程序的剩余部分”。参见以下两个小节：</p>
<ul>
<li><a>第六章：控制流#继续continuation</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/94611888">Continuation 在 JS 中的应用#continuation</a></li>
</ul>
<p>基于这一抽象，陆续发展出很多调度控制流的方法</p>
<h2 id="控制流抽象">控制流抽象</h2>
<h3 id="cps调度控制流的原始尝试">CPS：调度控制流的原始尝试</h3>
<p>早在 70 年代，一些程序员开始使用一种称为 CPS 的编程模式来优化编译器。</p>
<p>关于 CPS 的很好的介绍，可以参考 <a href="https://zhuanlan.zhihu.com/p/94611888">这一小节</a> 。此处仅给出结论：<strong>CPS 由于不适合人类编写和阅读，因此不是一种调度控制流的合适方法</strong>（但可以作为编译产物存在）。 但尽管如此，CPS 的幽灵仍然游荡在现代异步编程中，体现为 callback。</p>
<p>程序员们讨厌编写 callback（当然也有<a title="https://news.ycombinator.com/item?id=1549168" href="https://news.ycombinator.com/item?id=1549168">异议</a>），因为这里存在一处矛盾：<strong>业务逻辑和程序员的思路都是线性的，而 callback 却呈碎片状散落在各处</strong>。此外，还有<a title="https://www.zhihu.com/question/32218874/answer/216801915" href="https://www.zhihu.com/question/32218874/answer/216801915">种种不便</a>。</p>
<p>因此很自然地，程序员们需要一种更高级的语言级别的抽象——让语言来迎合人，而让解释器、编译器去处理机器的需要。</p>
<h3 id="callcc">callcc</h3>
<p>callcc，全称 call with current continuation，是<a title="https://www.zhihu.com/question/19585576/answer/19727369" href="https://www.zhihu.com/question/19585576/answer/19727369">功能集最小的</a>（这意味着<strong>一切关于控制流的抽象，都可以被当作是对 callcc 的某种封装</strong>）、用于调度控制流的语言抽象。相比 CPS，是一种对 continuatin 的高级应用。</p>
<p>可以将 callcc 理解为一种基于 continuation 的<a href="https://zhuanlan.zhihu.com/p/94611888">高级版的 goto</a>（而 goto 是简单地基于源码位置的），可谓万能而危险，因而难以广泛应用（参考 goto，在结构化编程语言中，基本上是一个被封印起来的语言特性）。以下两篇文章给出了很好的示例：</p>
<ul>
<li><a title="https://www.zhihu.com/question/21954238/answer/1829986581" href="https://www.zhihu.com/question/21954238/answer/1829986581">如何解释 Lisp 中 call/cc 的概念？ - 叶芝秋的回答 - 知乎</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/94611888">Continuation 在 JS 中的应用#callcc</a></li>
</ul>
<h3 id="协程coroutine">协程（coroutine）</h3>
<p>协程功能允许程序员（借助任意个协程，所谓的“用户态线程”）对一个控制流进行灵活的调度（这有别于线程机制，即让程序员开启数个同时活跃的控制流）。</p>
<p>协程是这样一种语言级抽象（在大部分语言中体现为一种特殊的函数）：其可以“让出”（用某种关键字，常见的有<code>yield</code>或者<code>await</code>）控制流，之后在合适的时机可以由程序员手动（<strong>无调度器</strong>，例如 JS 中的 generator，C# / Lua 中的 coroutine）或runtime 自动（<strong>有调度器</strong>，例如 C# / JS / Python 中的 async-await，Go 中的 goroutine）恢复控制流。“除非让出执行权，否则控制流不会被打断”，便是“协”的含义。</p>
<p>具体可见<a>第八章：子程序和控制抽象#协程</a>一节。</p>
<p>协程对于异步调用来说用处很大：当控制流来到异步调用处时，程序员可以利用协程机制，让控制流去往其它地方（而不是停滞不前等待调用返回）；待异步调用返回后，再让控制流回到此处。</p>
<p><strong>就主流实现而言，协程可分为两类：</strong></p>
<h4 id="无栈协程">无栈协程</h4>
<p>无栈（stackless）协程，顾名思义，即对于一个协程，不提供一个专门的栈空间供它记录环境，而是把<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">所有信息都记录到堆里</a>。此类协程的实现很简单，可以简单到仅仅是<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">编译器提供的 CPS /状态机语法糖</a>（在<a title="https://www.zhihu.com/question/305443189/answer/551716136" href="https://www.zhihu.com/question/305443189/answer/551716136"> runtime 里实现</a>当然也行）。</p>
<p>无栈协程的一个主要缺陷是<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">染色问题</a>。</p>
<h4 id="有栈协程">有栈协程</h4>
<p>有栈（stackful）协程，有时又叫纤程（Fiber。这个术语也<a href="https://zhuanlan.zhihu.com/p/94611888">被 React 借用了</a>），其特点是：<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">每个有栈协程都有一个独立的调用栈，且有栈协程间能互相切换</a>。这是靠“将每个有栈协程都放在一个独立线程中”来实现的。</p>
<p>对 Go 而言，这意味着使用协程（体现为 goroutine）时，可能存在着多个同时活跃的控制流；而对同为使用有栈协程的 Lua 而言，当一个协程内有控制流的时候，其余协程的控制流都处于暂停状态。</p>
<p>关于无栈协程和有栈协程在实现上的详细对比，可见<a href="https://www.zhihu.com/question/65647171/answer/233495694">此文</a>。</p>
<h4 id="其它">其它</h4>
<p>此外，还有<a title="https://www.zhihu.com/question/23955356/answer/732629313" href="https://www.zhihu.com/question/23955356/answer/732629313">stack-twine、stackcopy 等方式实现的协程</a>，但由于种种负面原因，均属于非主流实现，此处不一一介绍。</p>
<p>此外，<a title="https://www.zhihu.com/question/502314022/answer/2247370966" href="https://www.zhihu.com/question/502314022/answer/2247370966">这篇文章</a>对比了不同语言中的协程实现。</p>
<h3 id="代数效应algebraic-effects">代数效应（Algebraic Effects）</h3>
<p><strong>代数效应是这么一种语言特性：其相当于语言层面的依赖注入</strong>。目前只有极少的编程语言实现了这一特性。其功能是：当控制流来到值的注入点的时候，会立刻前往位于调用栈下方某处的、定义了该值的求值过程的地方。当求值完成后，控制流再返回注入点。</p>
<blockquote>
<p>（和协程对比）从程序运行本质上差不多。也和continuation passing style差不多。都是把当前运行环境中下一步要执行的代码(current continuation)，给保存起来，让外部传入东西继续执行。</p>
<p>再说区别，一般而言coroutine是one-shot的。比如generaor函数运行了next以后，就再也回不去了</p>
<p>而algebraic effect更灵活，是可以多次重入的。</p>
<p><a title="https://www.zhihu.com/question/506309867/answer/2273058569" href="https://www.zhihu.com/question/506309867/answer/2273058569">https://www.zhihu.com/question/506309867/answer/2273058569</a></p>
</blockquote>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Wet Codebase 讲座翻译]]></title>
            <guid>22674ba34aff4a16b3317f106503b43b</guid>
            <pubDate>Thu, 04 Nov 2021 13:29:39 GMT</pubDate>
            <content:encoded><![CDATA[<p><a title="https://www.bilibili.com/video/BV1Kv4y1f7hm/" href="https://www.bilibili.com/video/BV1Kv4y1f7hm/">https://www.bilibili.com/video/BV1Kv4y1f7hm/</a></p>
<blockquote>
<p>(Editor’s note: transcripts don’t do talks justice. This transcript is useful for searching and reference, but we recommend watching the video rather than reading the transcript alone! For a reader of typical speed, reading this will take 15% less time than watching the video, but you’ll miss out on body language and the speaker’s slides!)</p>
</blockquote>
<p>注意：演讲稿文本便于搜索和引用，但不推荐只看演讲稿，不看原视频。虽然只看演讲稿能省下 15% 的时间，但错过了演讲者的肢体语言和幻灯片（本文还删掉了一些演讲过程中抖的机灵——译注）。</p>
<blockquote>
<p>[APPLAUSE] Hi. I learned to drink a lot of water. Hi, my name is Dan Abramov. I work on a JavaScript library called React. This is actually the first conference that I speak at that is not specific to React with JavaScript. So I’m just curious, have any of you ever used React at all. OK. Yeah, a lot of people use React. That’s cool. This talk is not about React. You can say it’s a talk about something that, if I had a time machine and could come back to my past self, I would tell myself that talk. So it’s a talk about the code base far, far away, deep under the sea.</p>
</blockquote>
<p>大家好，我是 Dan Abramov，我一直在开发一个叫 React 的库。这个演讲不是关于 React 与 JavaScript 的，可以这么说，如果有一台时光机，我会回到过去，把这个演讲放给过去的自己看。这个演讲是关于一个遥远的、深海里的代码库的（译注：演讲者意在模仿童话故事的开头，而不涉及任何比喻或隐喻）。</p>
<blockquote>
<p>And it’s a code base that I worked on a long time ago. And in the code base, there were two different modules, two files. And my colleague and friend was working on a new feature in one of those files. And they noticed that actually that feature, something very similar was already implemented in another file. So they thought, well, why don’t I just copy and paste that code because it’s pretty much the same thing?</p>
</blockquote>
<p>这个代码库是我很久以前参与的。在那里面，有两个不同模块，就是两个文件。有一次，我和我同事要在其中一个文件里开发新功能，然后他们发现，另一个文件里已经有一个非常类似的实现了，所以他们觉得，既然基本是同样的功能，何不直接复制粘贴过来？</p>
<blockquote>
<p>And they ask me to review the code. And I just read all the books about the best practices. Pragmatic Programmer, Clean Coder, Well Groomed Coder, and I knew that I needed to-- you’re not supposed to copy and paste code because it creates a maintenance burden, it’s pretty hard to work with. I just learned this acronym DRY, which stands for don’t repeat yourself. And I was like this looks like a copy paste, so can we DRY it up a little bit?</p>
</blockquote>
<p>他们复制粘贴好后，让我做 code review。那段时间我刚好读了一大堆讲述编程最佳实践的书，我学到了不应该复制粘贴代码，因为这对后续维护造成了困难。我刚学到一个术语——DRY，don’t repeat yourself，看了同事的代码，就想，何不对这些复制粘贴的代码来个 DRY？</p>
<blockquote>
<p>And so my colleague was like, yeah, sure, I can totally extract that code to a separate module and make those two files depend on that new code. And so an abstraction was born. OK. So when I say abstraction, I mean it doesn’t matter which language you’re using. It could be a function or a class, a module, a package, something reusable that you can use from different places in your code base.</p>
</blockquote>
<p>然后我同事说，对，我确实可以把这段重复代码抽成一个单独的模块，然后让现有的两个文件都依赖它。这么一来，一个新的“抽象”就成了！注意了，我这里说的“抽象”，和任何编程语言无关，抽象的形式可能是函数、类、模块或者包，总之是某种你能在代码库的其它地方复用的东西。</p>
<blockquote>
<p>And so it seems like, this is great. And they live happily ever after. So let’s see let’s see how that abstraction evolved. So the next thing that happened, we hadn’t looked at that code for a while but then we were working on a new feature and it actually needed something very similar. So let’s say that the original abstraction was asynchronous, but we needed something that had pretty much the same exact shape, except it was synchronous.</p>
</blockquote>
<p>看上去不错哦！从此之后，大家过上了幸福快乐的生活，也不怎么管抽出去的那个模块了。然而好景不长，后来又来了个需求，这个需求和之前的那个又非常类似，这次的区别在于，要求的实现是同步的，而之前我们搞的那个是异步的。</p>
<blockquote>
<p>So we couldn’t directly reuse that code anymore, but it also felt really bad to copy and paste it because it’s pretty much exactly the same code except it’s slightly different. And, well, it looks like we shouldn’t repeat ourselves so let’s just unify those two parts and make our abstraction a bit fancier so that it can handle the case as well. And we felt really good about it. It is a bit unorthodox, but that’s what happens when code meets real life, right? You make some compromises, and at least we didn’t have to duplicate the code, because that would be bad, right?</p>
</blockquote>
<p>也就是说，我们无法直接复用之前的那个模块，但是复制粘贴一通基本一致的代码也很恼人。好吧，这次我们结合同步/异步这两种需求，把那个抽象搞得更通用一点。其实这种妥协的做法，在真实世界里每天都在发生，但至少我们没有重复的代码了。挺好！</p>
<blockquote>
<p>So what happened next is we found out that actually, this new code, this new feature, had a bug in it, and that bug was because we thought that it needs exactly some the same code as we have. But actually it needed something slightly different. But we can fix that bug, of course, by adding a special case. So our abstraction, we can have an if statement. If it’s like this particular case, then do something slightly different. Sure. Ship it. Because that happens to every abstraction, right?</p>
</blockquote>
<p>然而，后来我们发现，这段新功能代码里存在 Bug！Bug 的原因是，我们误以为两个功能的实现在代码上是一致的，然而不是！两个功能其实有一些小小的不同。不过，我们能搞定这个 Bug，只要在那段抽象里加一个特殊 case 处理的逻辑就行，简单来说就是加个 if 语句的事儿！OK，搞定了，毕竟每个抽象都会遇到这种麻烦事儿，我们属实是轻车熟路了。</p>
<blockquote>
<p>And so as we were working with that code, we actually noticed that the original code also had a bug. So those two cases that we thought were the same, they were also slightly different, we just didn’t realize it at the time. And so we added another special case. And at this point, this abstraction looks a bit weird and intimidating. So maybe lets make it more generic. Why do we have all those special cases in the abstraction?</p>
</blockquote>
<p>然而，在我们做这个改动时，又发现了那段代码里存在的类似问题！好吧，再加个 if 语句。此时，这段抽象看起来已经有点奇怪了，它处理了很多具体的 case，这不好。</p>
<blockquote>
<p>Let’s pull them out from the abstraction where they belong in our concrete use cases. So looks like this. So now our abstraction doesn’t know about any concrete cases. It is very generic, very beautiful. Nobody really understands what it represents anymore. Oh, by the way, we need to add, now that it’s parametrized from different places, we need to make sure that all code size are parametrized.</p>
</blockquote>
<p>那么，让我们把这些具体 case 的处理移出这段抽象吧。ok，现在这段抽象里不包含任何具体 case 的处理了，这段抽象现在非常通用，非常漂亮，通用到一般人都看不出它要干啥了。顺便一提，我们得给这个抽象加个参数，这个参数表示抽象在哪里被调用。</p>
<blockquote>
<p>But it was such a gradual progression that at each step it makes sense to the people writing and reviewing the code, so we just left it at that. And some time passed. And so, during that time, some people have left the team, some people have joined the team. There were many fixes. Somebody needed to just do this one small fix here. I don’t really know what this thing is supposed to be doing but just fix it up a little bit, add this new feature, improve the metrics. So we ended up with something like this, right?</p>
</blockquote>
<p>这一过程是随着代码库的开发渐渐发生的，不在话下。随着时间的推移，开发团队里有人离开，有人加入。虽然并不清楚那些抽象的具体作用，但是代码库里渐渐加入了各种 bugfix，feature，指标优化。事情总是这样，对吧。</p>
<blockquote>
<p>And again, each of those individual steps kind of made sense. But if you lose track of what you were trying to do originally, you don’t really know that you have a cyclical dependency or this weird thing that is growing somewhere to the side just because you don’t see the whole picture anymore. And, of course, in real life, that’s actually where the story ends because nobody wanted to touch the part of the code base and it just was stagnant for a long time and then somebody rewrote it. And maybe got a promotion. I don’t know.</p>
</blockquote>
<p>代码库内容的每一次改动，都有其意义。但如果你已经弄不清它们在一开始是做什么的，那么你就会碰到诸如循环引用的问题，或者各种奇奇怪怪的东西。毕竟，我们已经失去了对代码库的全景视野了。在现实世界中，通常这就是故事的结局，因为没人想动那部分的抽象，它们的迭代停滞了。直到有一天，有人重写了它们。或许重写使得变得它们变好了吧！我不确定。</p>
<blockquote>
<p>But if we could go back in time, because it’s a talk, it’s not real life, if we had a time machine we could go back and fix it, right? So I want to go back to the point where the abstraction still made sense. But if we had this third case and we really didn’t want to duplicate that code even though it needed something slightly different. And they were like, yeah, sure, let’s compromise on our abstraction. Make it funny. So this is if I from today was there, what I would’ve told myself is, please inline this abstraction.</p>
</blockquote>
<p>那么，假设我们有个时光机器，可以回到一开始，那我们就能挽回局面。让我们回到那个抽象的意义很清晰的那个时间点。在第三个 case 出现时，我会告诉那时候的自己，妥协吧，把那段抽象“内联”就完事了！</p>
<blockquote>
<p>And so what I mean by inline, I mean literally take that code and just copy and paste it back to the places that use it. And that creates some duplication but that destroys that potential monster we were in the process of creating. And of course duplication isn’t perfect in long term, but wrong abstraction is also not perfect in long term. So we need to balance these two problems. And so the way this helps us is that now if we have a bug here and we realized actually this thing is supposed to do something different, we can just change it. And it doesn’t affect any of the other places because it’s isolated. And similarly, maybe we get a different bug here and we also change it.</p>
</blockquote>
<p>我这里所谓的“内联”，就是复制粘贴。复制粘贴制造了重复代码，但是却把那头后来越长越大的怪兽扼杀在了摇篮里。当然，长期来看重复代码并不好，但又有平衡之处：如果我们发现这段功能其实要做的是另一件事，我们直接就地改动就完事了。就算引入了新 Bug，也能就地解决。</p>
<blockquote>
<p>And I’m not suggesting that you should always copy paste things. In longer term, maybe you realize that these pieces really stabilized and they make sense. And maybe you pull something out and it might not be the thing that you originally thought was a good abstraction. Might be something different. And a thing like this is as good as it gets in practice. And if I heard this when I was a sweet summer child, I would have said that that’s not what they tell us. I heard that copy pasting is really bad.</p>
</blockquote>
<p>我不是说我们就一直复制粘贴了事。长期来看，或许有一天你会发现，这些重复的片段的功能其实很稳定一致，然后，你会把它们抽出来。你会发现，此时抽出来的东西——一个经过实践考验的东西，和你当初想象中的抽象不太一样。如果把这一经验说给年少无知的我听，我可能不会承认，因为我一直被教育“复制粘贴坏坏”。</p>
<blockquote>
<p>And I think it’s actually a self-perpetuating loop. So what happens is that developers learn best practices from the previous generation and they try to follow them. Because there were concrete problems and concrete solutions that were born out of experience. And so the next generation tries to pass them on. But it’s hard to explain all this context and all this trade off, so they just get flattened into these ideas of best practices and anti-patterns.</p>
</blockquote>
<p>这是个永恒的循环：程序员们从前辈那里学习最佳实践，因为那些最佳实践确实是经受了具体问题、具体场景的考验的。然后程序员们再把这些知识传给下一代。其中，有些东西很难解释为什么，因为涉及到的是在具体场景下的取舍，留给后人的也只能是一条条现成结论了。</p>
<blockquote>
<p>And so they get taught to the new generation. But if the new generation doesn’t understand the trade offs and the reasons they came to these conclusions, they don’t have the context to decide when it’s actually a bad idea and how far can you stretch this. So they run into their own problems from trying to take these best practices and anti-patterns to extreme. And so they teach the next generation. And maybe this is just you can’t break out of this loop and it’s just bound to happen over and over again, which is maybe fine.</p>
</blockquote>
<p>那么，新一代程序员由于不了解这些结论是怎么来的，面对具体问题时，他们可能无法分辨这些结论到底管不管用，或者有多管用，这就导致了他们为了最佳实践而最佳实践（例如无条件 DRY——译注），最终碰了一鼻子灰。</p>
<blockquote>
<p>I think one way to try to break this loop is just when we teach something to the next generation, we shouldn’t just be two-dimensional and say here’s best practices and anti-patterns. But we should try to explain what is it that you’re actually trading away. What are the benefits and what are the costs of this idea? And so when we talk about the benefits of abstraction, of course it has benefits. The whole computer is a huge stack of abstractions. And I think concrete benefits are-- abstractions let you focus on a specific intent, right? So if you have this thing and they have to keep it all in their head.</p>
</blockquote>
<p>我认为，跳出这种循环的一种方法是，我们教下一代程序员时，不能光给出“这种做法好，那种做法不好”的二极管式结论，我们应当解释这一结论是怎么权衡得出的——当采纳这一结论时，你得到了什么，又失去了什么？说回“抽象”，抽象当然很好，毕竟整个计算机体系都是建立在抽象层次上的，我认为抽象的具体好处是：抽象让你专注于某个具体事务（而不被其它事情干扰——译注）。</p>
<blockquote>
<p>But it’s actually really nice to be able to focus on a specific layer. Maybe you have several places of code where you send an email and you don’t want to know how an email is-- I don’t know how emails are being sent. It’s a mystery to me that they even arrive. But I can call a function called send email and well, it works most of the times. And it’s really nice to be able to focus on it. And of course another benefit is just being able to reuse code written by you or other people and not remember how it actually works.</p>
</blockquote>
<p>假如你的代码里有几处地方是要发送电子邮件的，但你不知道电子邮件的发送原理，好在，你只需要调用一个能发送电子邮件的函数，就完事了（这样一来，你只需要专注于你的业务——译注）。抽象的另一个好处在于同样的功能能被你自己或者其他人重用，哪怕使用者并不知道/记得其中的原理。</p>
<blockquote>
<p>So if we need something, exactly the same thing that we already use from different places, it’s very nice to be able to reuse it. So that’s a benefit of abstraction. And abstraction also helps us avoid some some bugs. So in the example where we have a bug, maybe we copy pasted something. And that’s an argument against copy paste, is we copy pasted something and then we found the bug in one version and we fix it, but then the other version stays broken because we forgot about the copy paste. So that’s a good argument for why you’d want to extract something and pull it away.</p>
</blockquote>
<p>所以，假如我们要在代码的不同地方复用同一个功能，抽象是很有用的。抽象也能帮我们减少一些 Bug，假如我们一味复制粘贴，那么当我们修复一个地方的 Bug 时，其他地方则还留存着同样的 Bug。这就是我们有时要“抽出一些的东西”的正面理由。</p>
<blockquote>
<p>But when we talk about benefits we should also talk about costs. And so one of these costs is that abstraction creates accidental coupling. And what I mean by that is, so we have these two modules using some abstraction, and then we realize that one of them has a bug. And we have to fix it in the abstraction because that’s literally where the code is. But now it’s your responsibility to consider all of the other call sites of this abstraction and whether you might have actually introduced a fix in another, introduced the bug in another part of the code base. So that’s one cost. Maybe you can live with it. Most of us live with it. But it’s a real cost.</p>
</blockquote>
<p>但这么做也是有代价，其中之一是，抽象意外地导致了耦合。假如我们有两个模块依赖同一个抽象，当我们发现其中一个模块的功能有问题——哦，原来是它依赖的抽象有问题——进去修好了，那么，现在你就有责任回归这个抽象的所有调用方，确保这次的修复万无一失。或许你对此已习以为常，但是，这确实是一个代价。</p>
<blockquote>
<p>And I think an even more dangerous cost is the extra indirection an abstraction can create. So what I mean by that is that the promise was that I would just be able to focus on this specific layer in my code and not actually care about all the layers. Is that really what happens? I’m sure most of you probably had this bug where you started one layer, oh, it goes here. And it’s like, well, actually, no. You need to understand this layer and this other layer because the bug, it goes across all of those layers. And we have a very limited stack in our heads.</p>
</blockquote>
<p>另一个代价更为危险：抽象会带来 extra indirection（不好翻译，保留原文——译注）。这里的意思是，抽象通常许诺我们说，我们只要关心我们的这一层次就好，其它层次都可以藏在抽象后面。但这是真的吗？我确信你们都遇到过这种情况：你在某一层发现了 Bug，再仔细一查，哦，原来根源在另外一层。也就是说这个 Bug 使得你不得不理解当前层级以及另一层级，搞不好最后你得搞明白所有层级。然而我们的大脑容量说到底是有限的。</p>
<blockquote>
<p>And so what happens is you just get a stack will fall, which is probably why the site was coded that way. And so what I see happen a lot is that we try so hard to avoid the spaghetti code that we create this lasagna code where there are so many layers that you don’t know what’s going on anymore at all. So that’s extra indirection. And all of them wouldn’t be that bad if they didn’t entrench themselves.</p>
</blockquote>
<p>我常见到的一种情况是，我们为避免意大利面条式的代码引入了千层饼式的代码：代码库里的层级多到我们根本搞不清怎么回事了。这就是 extra indirection。当然，如果他们比较好改动，那也不算糟糕透顶。</p>
<blockquote>
<p>So abstraction also creates inertia in your code base. And that’s a social factor more than technical. What I’ve seen happen many times is you start with an abstraction that looks really promising and makes sense to you. And then with time it gets more and more complex. But nobody really has time to refactor or unwind this abstraction, especially if you’re a new person on the team. You might think that it would be easier to copy and paste it, but first you don’t really know how to do that anymore because you’re not familiar with that code. And second you don’t want to be the person who just suggests worst practices. Who wants to be the person who says, let’s use copy paste here? How long do you think you’re going to be on that team?</p>
</blockquote>
<p>综上，抽象总是在你的代码库里制造惰性。与其说这是技术因素，不如说是社会因素（我们总是不太想动别人写好的抽象——译注）。我常见到的一种情况是，你弄了个抽象，一开始它很好用，然而随着时间推移它越来越复杂，尤其对于新入团队的人来说。新人或许想直接复制粘贴，但那代码复杂得连复制粘贴都不知道从哪里开始，另外新人出于自尊也不想成为在代码库里复制粘贴的带恶人。</p>
<blockquote>
<p>So you just accept the reality for what it is and keep doing it and hope that this code is not going to be your responsibility anymore soon. And the problem is that even if your team actually agrees that the abstraction is bad and it should be inlined, it might just be too late. So what might happen is that you’re familiar with just this concrete usage and you know how to test it. If you unwind the abstraction, you can understand how to verify that change didn’t break anything. But maybe there is another team who uses it here and another team who uses it there, and maybe this team has been reorged so there is no team that maintains that code, and you don’t really know how to test it anymore. So you just can’t make that change even if you want to.</p>
</blockquote>
<p>最终，新人屈服于现实开始复制粘贴。问题在于，即使团队开始意识到应该内联抽象，也已经太迟了。就算你们你知道这个抽象的用处和测试方法，能确保在取消这个抽象后一切功能正常，但如果别的团队也在用这玩意儿，该怎么办？或者说你们的团队重组了，懂得这个抽象怎么测试的人已经不在了，你还敢动这个抽象吗？</p>
<blockquote>
<p>So I really like this tweet. It’s a bit hard to read. Easy-to-replace systems tend to get replaced with hard-to-replace systems, which is kind of like the Peters Principle. There’s this Peter’s Principle that everybody in the organization continuous raising until they become incompetent and then they can’t raise anymore. And it’s similar that if something is easy to replace, it will probably get replaced. And then at some point you hit the limit where it’s just a mess and nobody understands how it works.</p>
</blockquote>
<p>我喜欢这条有点拗口的推特：“一个易于被替代的系统，总是倾向于被难以替代的系统替代掉。”这有点像彼得斯定律：组织里的每个人都会不断升迁，直到他们变得不称职而无法再升迁为止。类似地，一个容易被替换的东西，很大概率会被替换，直到换上来的东西属实一团糟以至于没人懂里面的原理为止。</p>
<blockquote>
<p>So I’m not saying that you shouldn’t create abstractions. That would be a very two-dimensional or one-dimensional takeaway. I’m saying that there are things that, we’re going to make mistakes. So how can we actually try to mitigate or reduce the risks from those mistakes? And so one of them that I learned on the React team in particular is to test code that has concrete business value. So what I mean by that is, say we have this a little bit wonky abstraction, but we finally got some time to write some proper tests, because we fixed some bugs and we have a gap before the new half of the year starts and we can fix some things.</p>
</blockquote>
<p>再次强调，我不是说我们不应该编写抽象，那就太独断论了。我是说（在写抽象的过程中——译注）我们很可能会犯错。那么我们如何转移或是减少此类错误带来的风险呢？我在 React 团队里学到的一件事是，测试具有具体业务价值的代码（而不是抽象的模块——译注）。而对于那些似乎不太靠谱的抽象，我们只在它经历了几次 bugfix 之后，趁着新的半年度开始前，才给它加上一些测试。</p>
<blockquote>
<p>So we want to write some unit test coverage for that part. And intuitively, where I would put unit test is, well, here’s the abstraction where the complex code lies. So let’s put unit test to cover that code. And that’s actually a bad idea in my opinion, because what happens is that if later you decide that this abstraction was bad and you try to turn it into copy paste, well, guess what happens through your tests? They all fail. And now you’re like, well, I guess I’ll have to revert that because I don’t want to rewrite all my tests. And I don’t want to be the person who suggested to decrease the code coverage. So you don’t do that.</p>
</blockquote>
<p>假如我们想给代码库加点单元测试，从直觉上来说，应当给内部代码复杂的抽象加单测。实际上，在我看来这是不对的，因为假如之后你认定这个抽象不太好，并决定改用复制粘贴，那你的单测就作废了，一想到这，或许出于保持测试覆盖率的动机，你就想保持现状。</p>
<blockquote>
<p>But if you have a time machine you can go back and you can write your unit tests or integration tests or whatever you want to call them, fad of the day tests, against the code that we actually care about, that this code works against concrete features. And then there’s this test that don’t care about your abstraction. So you can inline the abstraction back. You can create five layers of abstraction. The test will tell you whether this code works. So actually they will guide you to refactor it because they can tell you that your refactoring is in fact a correct one. So testing concrete code is a good strategy.</p>
</blockquote>
<p>但如果你有时光机，你就能回到过去，让自己只对着真正值得关心的、有具体业务功能的代码写单元测试/集成测试/时下流行的这种那种测试。这样的话，你的测试就不涉及到那些抽象，内联抽象时也就没有顾虑了。哪怕你弄了个五层抽象，在重构它们时，这些测试也能确保万事大吉。综上，“只测试具体的代码”是个好策略。</p>
<blockquote>
<p>Another one is just to restrain yourself. You see this full request. You get this itch, like, this looks duplicate. And you’re like, no, take a walk. Because if you have this, you might have a high school crush and they are really into the same obscure bands on Last.fm that you’re into. That doesn’t mean that you have a lot in common and they’re going to be a good life partner. So maybe you shouldn’t do the same to the code. Just because the structure of these two snippets looks similar, it might just mean that you don’t really understand the problem yet. And give it some time to actually show that this is the same problem and not just accidentally similar code.</p>
</blockquote>
<p>另一个要点在于约束你自己。当你看到一个需求时，你心里可能痒痒的：啊，这和之前的某个功能很类似，让我们抽出来！此时，赶紧停下来，去散个步冷静下。因为这种感觉很可能类似你的高中恋爱，你和 TA 仅仅是喜欢同一个摇滚乐队，就好像是天造地设的终身伴侣一样！对你的代码，可别犯下这种错误。两段代码结构看着相似，说明不了什么，很可能只是因为你没发现问题所在罢了。让时间来验证这两段代码是否真的解决了同一个问题，而非只是偶然相似罢了。</p>
<blockquote>
<p>And finally, I think it’s just important that if that happens, if you make a mistake, it should be part of your team culture to be OK with, this abstraction is bad. We need to get rid of it. You should not only add abstraction, but you should also delete them as part of your healthy development process. So that means that it should be OK to leave a comment like this and say, hey, this is getting out of control. Let’s spend some time to copy and paste this and later we’ll figure out what to do with it.</p>
</blockquote>
<p>最后，我发现还有一点很重要：你的团队的文化应当允许犯错，能大方地说，哦，这个抽象不咋地，还是弄掉吧。在抽象方面，你们要做加法，更要做减法，这才称得上健全。具体来说，代码库里应当有诸如“嘿，这个抽象把事情弄复杂了。我们先复制粘贴，之后再看看能不能改”之类的评论。</p>
<blockquote>
<p>But there is also a technical component to this. So if your dependency tree looks like this, it might actually be really challenging to inline anything because you’re like, well, I have this thing I want to inline but, OK, I can copy it, but there’s some mutable shared state that is now being duplicated. And I need to figure out how to rewire all of those dependencies together. And it might not even be feasible. So you just give up. And I don’t really have a good solution for this. What I’ve noticed is that, for some code, you can’t really avoid it. For example, in the source code of React itself, we do have a problem like this. Because we try to mutate things for you so you don’t have to mutate them. So we have all this interdependencies between modules that can be a bit difficult to think about.</p>
</blockquote>
<p>这里面还有一些技术因素在作梗。如果你项目的依赖图长这样：
<img src="/_resources/50ec8d82205042bcb782d387b04b8d4a.png" /></p>
<p>那内联任何东西都是非常困难，甚至无法实现的，最后只能作罢。对于这种情况，我也没啥办法，只能说，确实存在某些场景（例如 React 内部的实现）是无法避免这种情况的。</p>
<blockquote>
<p>But then what’s cool about React, in my opinion, is that it lets you write apps with dependency trees that are more like this. So you have a button component that’s used from form, and that form is used from app. And so on like this. And it follows this tree shape. And we have these constraints for data flows only in one direction. So you don’t really expect things to get weird circular. And what it means is that you’re going to make mistakes, you’re going to create bad abstractions, but does your technology make it easier for you to get rid of them?</p>
</blockquote>
<p>React 很棒的一点在于，React 项目牺牲自己，只为成全广大用户的项目不出现上图的那种情况。React 是单向数据流的，因此出现循环引用的概率不大。重点在于，你会犯错，会弄出糟糕的抽象，但你的技术方案是否让你更容易紧急回避它们？</p>
<blockquote>
<p>Because I think with React components and some other constrained forms of dependency, like management, you have this nice property where it’s usually a matter of copy and pasting things in order to inline them. And so even if you make a bad decision, you can actually undo it before it gets too late. So this is something to consider in both social and technology part of it. So don’t repeat yourself. DRY is just one of those principles that are probably pretty good ideas.</p>
</blockquote>
<p>我认为，React 组件以及 React 施加于你的某些限制，为你的项目带来一个很好的特性：复制粘贴通常行得通。所以即使你弄了个坏抽象，也能在事态恶化之前挽回。</p>
<p>总之，这个问题既涉及到技术因素，也涉及到社会因素。DRY 原则，和其他许多理论一样，是一种“在大部分情况下管用”（也就是说，某些情况下不管用——译注）的理论。</p>
<blockquote>
<p>And there are many good ideas that you might hear about as a developer and entering this industry. Or even as somebody who’s been doing it for 15 years and then stepping outside for a few months. And we see a lot of evangelism around those things. And that is fine. But I think it’s important that when we try to explain what those things do or why they’re a good idea, we should always explain what exactly are you trading away and which things led us to that to that principle or idea. And what is the expiration date for those problems? Because sometimes there is some context that is assumed and that context actually changes but you don’t realize that. And so the next generation needs to understand what exactly was traded off and why.</p>
</blockquote>
<p>除此之外，你应该还从业界听过很多好观点，其中有些甚至甚至已经实践了十几年，拥有许多传道士。这都 ok，但我认为最重要的是我们必须解释清楚到底好在哪儿，坏在哪儿，其中有哪些权衡，以及，这些观点所解决的问题如今是否还存在？其中有些观点的语境早已过期，而后来者却没意识到这点。</p>
<blockquote>
<p>And so my challenge for you is to pick some best practices and anti-patterns that you strongly believe are true, whether from your experience or because somebody told you or because you came up with them, and really try to break it down and deconstruct why you believe these things and what exactly is being traded away. And if you found this talk interesting, you might like these other talks. So All the Little Things by Sandi Metz is an amazing talk that goes into way more detail on these ideas and many others. Minimal API Surface Area is a talk by my colleague, Sebastian, who I learn all of this stuff from. And On the Spectrum of Abstraction is an interesting talk by Cheng Lou, who goes into how abstractions help us trade the power and expressiveness for constraints and how those constraints can actually limit us, but let us do things we wouldn’t be able to do otherwise. It’s a good talk. And thank you for having me. That’s all I have.</p>
</blockquote>
<p>我在这里向大家提出个挑战：找一些你深信不疑的所谓的最佳实践或是反模式——无论是来自你的实践经验还是听别人说的——然后试着违反它们，看看实验结果是否会摧毁你曾深信的东西，让你弄明白其中的取舍。如果你对这个话题有兴趣，也能看看这些材料：</p>
<ul>
<li>Sandi Metz 的一些讨论</li>
<li><em>Minimal API Surface Area</em> by Sebastian</li>
<li><em>On the Spectrum of Abstraction</em> by Cheng Lou</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[“微前端”辨析]]></title>
            <guid>306e623ed01847de8cfab96749795ea9</guid>
            <pubDate>Fri, 08 Jan 2021 07:50:50 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="理论">理论</h1>
<h2 id="微前端运行时复用方案及其适用场景">微前端：运行时复用方案及其适用场景</h2>
<p>微前端这个概念的定义极为混乱，导致社区中谈论微前端时，颇有自说自话的感觉。本文旨在厘清目前社区中存在的对“微前端”一词的不同解释。</p>
<p>总的来说，对微前端的各种解释的一个最大公约数，是指一种<strong>在运行时加载欲复用的代码的方法，并且你项目的编译工具并不知道被复用代码的存在，这意味着——待复用的代码虽然未必可以独立使用，但是是可独立部署的（有独立的部署流程，最终有一个可访问的 URL）</strong> 。因此传统的“根据路由动态加载组件”之类的方案，甚至更早的 <a>module loader（如 require.js 之类的）</a>，虽然也是运行时加载复用代码，但不是微前端方案。</p>
<p>对于以下场景，运行时复用比传统的构建期复用更好：</p>
<ul>
<li>欲复用的代码源码不在你项目的 repo 里，因此无法在构建期复用。这可能是因为：
<ul>
<li>代码是其它团队的非公开代码</li>
<li>属于无法合并为 monorepo 的另一个项目</li>
<li>你嫌项目 repo 太大，正把这段代码移往别的 repo</li>
</ul>
</li>
<li>欲复用的代码在你项目的 repo 里，但由于采用了不同的技术栈等原因，构建期复用比较麻烦</li>
<li>欲复用的代码（例如，组件库）实际上被大量不同的项目同时复用，一旦改动，依赖于它的项目都需要重新编译、发布</li>
</ul>
<p><a title="https://twitter.com/dan_abramov/status/1132495687957012481" href="https://twitter.com/dan_abramov/status/1132495687957012481">Dan 认为</a>，微前端方案更像是由于团队原因而做的技术上的迂回。</p>
<p><img src="/_resources/768b073e8e0444b79fc905c36e7831d9.png" /></p>
<p>qiankun creator 的<a href="https://zhuanlan.zhihu.com/p/391248835">这篇文章</a>也指出了微前端本质是个团队架构问题。</p>
<h2 id="微前端的两种主流解释">微前端的两种主流解释</h2>
<p>现阶段（2021年底），在上述公约数的基础之上，社区里对“微前端”一词有两种主流解释。这两种解释的区别在于代码的复用粒度。</p>
<ul>
<li><strong>解释一：微前端 = 运行时动态加载执行 JS 模块</strong>。在英文社区中，由后端术语“micro-service 借鉴而来的术语 “micro-frontend” 基本上是指这种解释
<ul>
<li>代表框架： single-SPA</li>
<li>框架定位：<a title="https://single-spa.js.org/docs/faq/#what-does-single-spa-do" href="https://single-spa.js.org/docs/faq/#what-does-single-spa-do">运行时模块（组件、utils）加载器</a></li>
</ul>
</li>
<li><strong>解释二：微前端 = 运行时动态加载执行 App</strong>。在中文社区中，受 qiankun 的影响，术语“微前端”基本是指这种解释
<ul>
<li>代表实现：qiankun</li>
<li>框架定位：升级版的 iframe</li>
</ul>
</li>
</ul>
<p>搜索社区文章时应当注意，<a title="https://micro-frontends.org/" href="https://micro-frontends.org/">有些文章</a>（通常是英文社区的文章）使用解释一；有些文章使用解释二（通常是中文社区的文章）。有些文章混用了两种解释。</p>
<h3 id="联系">联系</h3>
<p>这两种解释并未泾渭分明，而是有所联系的——<strong>App 级的复用粒度，也可以视为模块级复用（将 main.js 视为模块）</strong>。正因如此，qiankun 是基于 single-SPA 的封装。</p>
<h3 id="区别">区别</h3>
<h4 id="直接动机">直接动机</h4>
<ul>
<li>模块级复用（single-SPA）：由于<strong>工程原因</strong>，你需要跨 repo 或跨技术栈的代码模块复用</li>
<li>App 级复用（qiankun）：因为<strong>存量的业务原因</strong>，你需要集成多个现有的独立 App；因为<strong>存量的业务或工程原因</strong>，你想拆分一个大 App 为多个小的独立 App，再集成</li>
</ul>
<h4 id="主要关注点">主要关注点</h4>
<ul>
<li>模块级复用（single-SPA）：模块生命周期、模块加载（JS Entry）、依赖共享（<a title="https://single-spa.js.org/docs/recommended-setup/#shared-dependencies" href="https://single-spa.js.org/docs/recommended-setup/#shared-dependencies">shared dependencies</a>）、<a title="https://single-spa.js.org/docs/recommended-setup/#inter-app-communication" href="https://single-spa.js.org/docs/recommended-setup/#inter-app-communication">模块间通信</a>、模块 API 版本管理</li>
<li>App 级复用（qiankun）：App 生命周期、App 资源（预）加载（<a href="https://zhuanlan.zhihu.com/p/369414267">HTML Entry</a>）、<a>CSS / JS 沙箱</a>、<a href="https://mp.weixin.qq.com/s/TAXP7ipDdtb2Jb-L3QHszA">主/子应用路由</a>等</li>
</ul>
<h2 id="微前端的其它解释">微前端的其它解释</h2>
<ul>
<li>宏大叙事型：常见于英语社区中。大体上描述了某种前后端协同渲染、分发页面组件的方案，本文不进行详细介绍，可以参阅：
<ul>
<li><a title="https://github.com/puzzle-js/puzzle-js" href="https://github.com/puzzle-js/puzzle-js">puzzle.js</a></li>
<li><a title="https://www.mosaic9.org/" href="https://www.mosaic9.org/">Project Mosaic</a></li>
<li><a title="https://opencomponents.github.io" href="https://opencomponents.github.io">Open Component</a></li>
<li><a title="https://fmfe.github.io/genesis-docs/" href="https://fmfe.github.io/genesis-docs/">Genesis</a>（国产）</li>
</ul>
</li>
<li>待续…</li>
</ul>
<h2 id="牢记微前端不可作为业务架构选型的一种选项">牢记：微前端不可作为业务架构选型的一种选项！</h2>
<p>正如前文反复强调的那样，微前端方案（无论何种解释），本质上是受工程现状或团队权责问题困扰的无奈之举，<strong><a href="微前端是技术负债吗？ - 若愚的回答 - 知乎 https://www.zhihu.com/question/480072987/answer/2084795116">是一种解决存量问题的方案</a>，通常不可将其作为全新项目中“进行业务划分的架构选型”的选项</strong>（除非你的项目在立项之初就碰到了项目团队的权责问题）。对于新项目，你的真实问题其实是——<strong>如何处理领域模块</strong>。可参考这几篇：</p>
<ul>
<li><a href="微前端是技术负债吗？ - 知乎 https://www.zhihu.com/question/480072987/answer/2065774545">微前端是技术负债吗？- 匿名用户的回答</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/342843429">简单两步，教你所谓的’微前端’</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/342416778">谈谈‘微前端’</a></li>
</ul>
<p>一句话：<strong>在新项目中，考虑引入“微前端”的场景应当是业务无关的性能优化/部署发布流程优化</strong>。</p>
<p>此处列出一些在我看来，实质上是“试图解决一些本不存在的问题”的方案。它们共同的一个显著特点是，都提供了 CLI 工具供用户在创建新项目时使用，并让用户把这些项目组合起来形成“微前端”：</p>
<ul>
<li><a title="https://docs.piral.io/guidelines/tutorials/01-introduction" href="https://docs.piral.io/guidelines/tutorials/01-introduction">Piral</a></li>
<li><a title="https://github.com/efoxTeam/emp" href="https://github.com/efoxTeam/emp">EMP</a>（国产）</li>
<li><a href="https://zhuanlan.zhihu.com/p/386120404">Fronts</a>（国产）</li>
<li><a title="https://github.com/SAP/luigi" href="https://github.com/SAP/luigi">luigi</a></li>
<li><a title="https://frint.js.org/" href="https://frint.js.org/">frintjs</a></li>
</ul>
<p>最后，即使是为处理存量问题，也应该意识到，微前端只是一个选项，而不是标准答案。此外，绝对不能使用任何微前端方案加载不可 100% 信任的第三方代码。</p>
<h1 id="工程">工程</h1>
<h2 id="无框架实现">无框架实现</h2>
<ul>
<li><a href="https://tech.meituan.com/2020/02/27/meituan-waimai-micro-frontends-practice.html">微前端在美团外卖的实践</a></li>
<li><a href="https://tech.meituan.com/2018/09/06/fe-tiny-spa.html">用微前端的方式搭建类单页应用</a></li>
</ul>
<p>美团的这两篇文章介绍了不使用框架，手动进行 App 级微前端项目改造的过程。</p>
<h2 id="框架实现">框架实现</h2>
<h3 id="框架介绍single-spa">框架介绍：single-SPA</h3>
<p>更详细的介绍可见 <a href="https://zhuanlan.zhihu.com/p/378346507">【微前端】single-spa 到底是个什么鬼</a> 一文</p>
<h4 id="single-spa-的作用">single-SPA 的作用</h4>
<p>sinle-SPA 的只起一个简单的唯一作用：<strong>在运行时用用户给定的加载方法，加载 JS 模块，并执行上面暴露出的生命周期钩子</strong>。</p>
<p>可见，single-SPA 甚至没提供<a>加载器（loader）</a>的实现（虽然以官方推荐的形式建议大家<a title="https://single-spa.js.org/docs/recommended-setup#systemjs" href="https://single-spa.js.org/docs/recommended-setup#systemjs">用 SystemJS</a>）。qiankun 使用了自己实现（而不是 single-SPA 官方推荐的 SystemmJS）的一套基于 umd 格式的加载方案（见源码文件 <a title="https://github.com/umijs/qiankun/blob/master/src/loader.ts" href="https://github.com/umijs/qiankun/blob/master/src/loader.ts">loader.ts</a>）。</p>
<h4 id="single-spa-的三类模块">single-SPA 的三类模块</h4>
<p>single-SPA 划分了三类可复用的模块：Application、Parcel、Utility</p>
<table>
<thead>
<tr>
<th></th>
<th>Application</th>
<th>Parcel</th>
<th>Utility</th>
</tr>
</thead>
<tbody>
<tr>
<td>模块规模</td>
<td>大。通常是某个 App 的 main.js</td>
<td>可大可小</td>
<td>小</td>
</tr>
<tr>
<td>专门的 API</td>
<td>有</td>
<td>有</td>
<td>无。<a title="https://single-spa.js.org/docs/recommended-setup#utility-modules-styleguide-api-etc" href="https://single-spa.js.org/docs/recommended-setup#utility-modules-styleguide-api-etc">用户需自行调用加载逻辑，并正确配置 Webpack 进行加载</a></td>
</tr>
<tr>
<td>根据路由触发加载/卸载</td>
<td>是</td>
<td>否</td>
<td>否</td>
</tr>
<tr>
<td>生命周期</td>
<td>single-SPA 自动控制（也支持手动 unload）</td>
<td>手动控制</td>
<td>无</td>
</tr>
<tr>
<td>额外能力</td>
<td>被注入了加载 Parcel 的能力（且自动同步生命周期）</td>
<td>无</td>
<td>无</td>
</tr>
</tbody>
</table>
<p>综上，Application 配置简单功能更多，Parcel 控制灵活。基于 single-SPA 的 qiankun ，有时将子应用视为 single-SPA 的 Application（当根据声明式路由进行加载时），有时则是 Parcel（当使用命令式进行加载时）。</p>
<h3 id="框架介绍qiankun">框架介绍：qiankun</h3>
<p>作为 App 级微前端框架的代表性作品，qiankun 的功能是此类框架的标准功能。可见 <a href="https://zhuanlan.zhihu.com/p/379744976">【微前端】qiankun 到底是个什么鬼</a> 一文。</p>
<p>类似的框架还有（不出意外，都是国产）：</p>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/393533835">MicroApp</a></li>
<li><a title="https://garfish.top/" href="https://garfish.top/">garfish</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/169800579">berial</a></li>
<li><a title="https://alfajs.io/" href="https://alfajs.io/">Alfa</a></li>
<li><a title="https://github.com/phodal/mooa" href="https://github.com/phodal/mooa">mooa</a></li>
<li><a title="https://github.com/worktile/ngx-planet" href="https://github.com/worktile/ngx-planet">ngx-planet</a></li>
<li><a href="https://www.tangshuang.net/7920.html">mfy</a></li>
<li><a title="https://micro-frontends.ice.work/" href="https://micro-frontends.ice.work/">icestark</a></li>
<li><a href="https://tech.meituan.com/2019/12/26/meituan-bifrost.html">Bifrost</a>（未开源）</li>
</ul>
<p>它们的目的基本一致—— 即 App 级复用。个别功能/实现/目标框架略有不同。例如 MicroApp, mfy 主打无侵入性，<a title="https://github.com/aliyun/alibabacloud-alfa/issues/51" href="https://github.com/aliyun/alibabacloud-alfa/issues/51">Afla 主打依赖共享</a>等。</p>
<h2 id="依赖共享及其方案">依赖共享及其方案</h2>
<p>依赖共享指的是，当用微前端方案在运行时加载模块 / App 时，防止重复加载相同的内容（如模块、依赖库等等）。甚至有人认为，<strong>微前端基本上等价于依赖共享</strong>（这种观点着重强调低运行时开销的复用能力）。</p>
<p>此处介绍依赖共享的两种实现方案（<a href="https://zhuanlan.zhihu.com/p/234964127">这两种方案可任意与 single-spa / qiankun</a>或其它微前端框架进行集成）：</p>
<ul>
<li><a title="https://github.com/systemjs/systemjs/blob/master/docs/import-maps.md" href="https://github.com/systemjs/systemjs/blob/master/docs/import-maps.md">SystemJS 的 import maps</a></li>
<li><a title="https://webpack.js.org/concepts/module-federation/" href="https://webpack.js.org/concepts/module-federation/">Webpack 5 的模块联邦</a>。它甚至声称自己本身就是一个微前端方案（它确实满足本文开头概括的“微前端”公约数）</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[现代 CSS 工程方案]]></title>
            <guid>d7506bf2f8fb4eb989885bde1a0bbbc4</guid>
            <pubDate>Fri, 30 Oct 2020 02:48:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="前言">前言</h1>
<h2 id="内容-样式-行为要分离吗">内容、样式、行为要分离吗？</h2>
<p>内容、样式和行为分离是前现代的前端开发口号，它并没有错误，但是已经过时了：本质上来说，这一口号是适用于<strong>排版时代</strong>的——在那个时期，网页开发被视为是一种排版工作。</p>
<p>在当今这个组件化开发 Web App 的时代，一个组件的内容、样式、行为如果不被放在一起，才是真正的疯狂。这就是为何各种前端程序员的 .js 文件中越来越充斥着 JSX/template 代码、CSS 代码。对于 CSS 而言，其很多功能也是诞生自排版时代的，例如 CSS 选择器、float、文档流等概念。这些功能在现代的 Web App 开发人员看来，便多少有点不合时宜了。</p>
<h2 id="传统做法及其问题">传统做法及其问题</h2>
<p>传统上，编写 CSS 代码的操作过程是：<strong>纵览文档结构（可选） -&gt; 为元素起名字（可能使用 <a title="https://www.zhihu.com/question/21935157/answer/20116700" href="https://www.zhihu.com/question/21935157/answer/20116700">BEM</a>、<a title="https://github.com/stubbornella/oocss/wiki" href="https://github.com/stubbornella/oocss/wiki">OOCSS</a>  等命名规范） -&gt; 用选择器选中 -&gt; 编写 CSS（或者 Sass / Less 等）语句</strong>。这一过程在现代前端项目中，有些不合时宜：</p>
<ul>
<li>由于组件化开发，前端工程师大多数时候面对着的是某一个（作为组件渲染产物的） HTML 片段。因此<strong>传统步骤的前三步，要么已经没有存在的基础，要么纯粹是添麻烦</strong></li>
<li>CSS 总是全局的。<strong>JS 代码早已可被封装为模块，CSS 代码却不行</strong>。这导致在大型应用中：
<ul>
<li>CSS 规则很可能互相干扰（选择器权重问题、命名冲突问题），带来混乱</li>
<li>由于不确定会发生什么，无人敢删 CSS 代码。于是项目里的 CSS 代码永远只增不减，成为一个烂摊子</li>
<li>CSS 代码不易分割以实现动态加载</li>
<li>组件的 CSS 代码分发不易。编写要分发的 UI 组件时要非常小心，不能对组件用户的页面中的其它部分造成影响</li>
</ul>
</li>
</ul>
<p>为了克服这些问题，现代的、有技术实现的各类 CSS 工程方案出现了。</p>
<h1 id="框架生态提供的方案">框架生态提供的方案</h1>
<h2 id="vue-官方的-scoped-css-方案">Vue 官方的 scoped CSS 方案</h2>
<p>Vue 生态通过提供 <a title="https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles" href="https://vue-loader.vuejs.org/guide/scoped-css.html#mixing-local-and-global-styles">Webpack Vue-loader</a>，向她的用户提供 scoped CSS 方案。其主要原理是，在打包时，对于被标注为<code>scoped</code>的 style，其中的所有选择器被额外加上随机字符串<code>[data-v-xxxxx]</code>，template 中的所有 HTML 元素和子组件的根 HTML 元素均被加上相应的<code>data-v-xxxxx</code> HTML 属性。也就是说<strong>通过为每个选择器附着一个属性选择器来实现模块化。</strong>（但不是彻底的模块化：子组件也会被影响）</p>
<p>从<a title="https://mp.weixin.qq.com/s/MJScjoqGtKh9IuFpfMbbQg" href="https://mp.weixin.qq.com/s/MJScjoqGtKh9IuFpfMbbQg">实现上</a>，是利用了<code>postcss-selector-parser</code>这个工具解析 scoped style 中所有的 CSS Selectors，然后逐一加上随机字符串<code>[data-v-xxxxx]</code>；template 中元素属性的添加则由<code>@vue/component-compiler-utils</code>来完成。</p>
<h2 id="angular-官方的-viewencapsulation">Angular 官方的 <code>viewEncapsulation</code></h2>
<p>编写 Angular 组件时，可以配置<a title="https://dzone.com/articles/what-is-viewencapsulation-in-angular" href="https://dzone.com/articles/what-is-viewencapsulation-in-angular">组件的<code>viewEncapsulation</code>选项</a>。根据配置值的不同，Angular 采用了以下不同的方案：</p>
<ul>
<li>Native 方案：Angular 将该组件渲染为浏览器的 shadow dom，样式也置于其中。shadow dom 自带 CSS 隔离的功能</li>
<li>Emulated 方案（默认方案）：原理同 Vue 的 scoped CSS 方案</li>
</ul>
<h2 id="react-社区的各类-css-in-js-方案">React 社区的各类 CSS-in-JS 方案</h2>
<p>CSS-in-JS （后文简称为 CIJ）在 2014 年由 Facebook 的员工 <a title="https://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html" href="https://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html">Vjeux 在 NationJS 会议上</a>提出。简单来说，<strong>其形式就是就是将组件应用的 CSS statement 写在 JavaScript 文件里面。</strong></p>
<p>CIJ 不是某套特定技术方案，恰恰相反，CIJ 的思路和技术实现众多（因为 <a title="https://reactjs.org/docs/faq-styling.html#what-is-css-in-js" href="https://reactjs.org/docs/faq-styling.html#what-is-css-in-js">React 官方并不关心这个问题，而是交给了社区</a>）。可以在 <a title="https://www.cssinjsplayground.com/" href="https://www.cssinjsplayground.com/">CSS-in-JS Playground</a> 上快速尝试不同的 CIJ 实现。</p>
<p>然而，在接口 API 设计、功能或是使用体验上，不同的实现方案越来越接近，其中最受欢迎的两个解决方案是 <a title="https://github.com/emotion-js/emotion" href="https://github.com/emotion-js/emotion">Emotion</a> 和 <a title="https://styled-components.com/docs/basics#motivation" href="https://styled-components.com/docs/basics#motivation">styled-components</a>。通过几年间的竞争和更新，它们渐渐具有了几乎相同的 API，只是在内部实现上有所不同。</p>
<h3 id="两种-api-风格">两种 API 风格</h3>
<p>各类 CIJ 方案提供的 API 通常是以下两种风格之一（或全部）：</p>
<ul>
<li>CSS props：将 CSS 代码以 prop 的形式施加在组件上
<ul>
<li>优点：很直观。没有作用域问题或是选择器权重问题</li>
<li>缺点：样式代码大量混杂在 JSX 中，可能降低代码可读性</li>
</ul>
</li>
<li>styled component：用户提供 CSS 代码，CIJ 库返回携带了 CSS 代码的组件供用户使用
<ul>
<li>缺点：用户得额外和各类纯样式组件打交道</li>
<li>优点：纯样式组件易于分发。适合有统一设计语言的团队</li>
</ul>
</li>
</ul>
<h3 id="实现细节">实现细节</h3>
<p>从实现方法上区分，大体分为两种流派：</p>
<ul>
<li>运行时方案</li>
<li>编译期方案</li>
</ul>
<h4 id="运行时方案">运行时方案</h4>
<h5 id="styled-components-实现赏析">Styled-components 实现赏析</h5>
<p>styled-components 在 API 风格上主要采用 styled component。</p>
<p>通过 styled-components，你可以使用 ES2015 的<a title="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">标签模板字符串</a>语法（Tagged Templates）创建一个 styled component，当该组件的 JS 代码被解析执行的时候，styled-components 会动态生成一个 CSS 选择器，并把对应的 CSS 样式通过 style 标签的形式插入到 head 标签里面。动态生成的 CSS 选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突。</p>
<p>除了styled-components，采用唯一 CSS 选择器做法的实现还有：<a title="https://github.com/cssinjs/jss" href="https://github.com/cssinjs/jss">jss</a>，<a title="https://github.com/emotion-js/emotion" href="https://github.com/emotion-js/emotion">emotion</a>，<a title="https://github.com/paypal/glamorous" href="https://github.com/paypal/glamorous">glamorous</a>等。</p>
<h5 id="radium-实现赏析"><a title="https://github.com/FormidableLabs/radium" href="https://github.com/FormidableLabs/radium">Radium</a> 实现赏析</h5>
<p>Radium 在 API 风格上采用的是 CSS props。</p>
<p>Radium 和 styled-components 的最大区别是它生成的是内联样式。由于标签内联样式在处理诸如<code>media query</code>以及<code>:hover</code>，<code>:focus</code>，<code>:active</code>等和浏览器状态相关的样式的时候非常不方便，所以 radium 为这些样式封装了一些标准的接口以及抽象。</p>
<h4 id="编译期方案">编译期方案</h4>
<h5 id="linaria-实现赏析"><a title="https://github.com/callstack/linaria" href="https://github.com/callstack/linaria">linaria</a> 实现赏析</h5>
<p>暂略。</p>
<h5 id="compiledcss-in-js-实现赏析"><a title="https://compiledcssinjs.com/" href="https://compiledcssinjs.com/">@compiled/css-in-js</a>  实现赏析</h5>
<p>暂略。</p>
<h3 id="各类-cjs-实现的其它功能">各类 CJS 实现的其它功能</h3>
<p>除了一些最基本的诸如CSS局部作用域的功能，下面这些功能有的实现会包含而有的却不支持：</p>
<ul>
<li>自动生成浏览器引擎前缀 - built-in vendor prefix</li>
<li>支持抽取独立的CSS样式表 - extract css file</li>
<li>自带支持动画 - built-in support for animations</li>
<li>伪类 - pseudo classes</li>
<li>媒体查询 - media query</li>
<li>其他</li>
</ul>
<p>可以看一下这个整理了不同实现的<a title="https://github.com/michelebertoli/css-in-js#features" href="https://github.com/michelebertoli/css-in-js#features">对比图</a>来比较它们的功能差异。</p>
<h3 id="cjs-的优缺点">CJS 的优缺点</h3>
<p>比起前文介绍的各种方案，CJS 是相当激进的一种：它消灭了<a title="#%E4%BC%A0%E7%BB%9F%E5%81%9A%E6%B3%95%E5%8F%8A%E5%85%B6%E9%97%AE%E9%A2%98" href="#%E4%BC%A0%E7%BB%9F%E5%81%9A%E6%B3%95%E5%8F%8A%E5%85%B6%E9%97%AE%E9%A2%98">传统步骤</a>的前三步。这种激进性带来一些好处和坏处。</p>
<p><strong>好处</strong>：</p>
<ul>
<li>CSS statement 完全是可编程的。其它方案通常需要通过对元素的 className 进行编程来迂回。（ Vue 3.2 起，也支持<a title="https://v3.vuejs.org/api/sfc-style.html#state-driven-dynamic-css" href="https://v3.vuejs.org/api/sfc-style.html#state-driven-dynamic-css">可编程的 CSS statement</a>）</li>
</ul>
<p><strong>坏处</strong>：</p>
<ul>
<li>all in js 破坏了前端开发的传统。具有一定的学习成本</li>
<li>采用运行时的方案需要在应用中额外加载 CIJ runtime，这带来额外的网络开销；在运行时操作 CSSOM，则会削减了应用的运行时性能</li>
<li>“唯一 CSS 选择器”的 CIJ 实现，其产物的可读性较差</li>
</ul>
<h3 id="从-react-社区到其它社区">从 React 社区到其它社区</h3>
<p>上文提到到很多 CIJ 方案也被移植到 Vue 社区等。例如 <a title="https://github.com/styled-components/vue-styled-components" href="https://github.com/styled-components/vue-styled-components">vue-styled-components</a> 等。此处不再赘述。</p>
<h1 id="框架无关的通用方案">框架无关的通用方案</h1>
<h2 id="css-module">CSS Module</h2>
<p><a title="https://github.com/css-modules/css-modules" href="https://github.com/css-modules/css-modules">CSS Module</a> 是社区对 CSS 语法和语义对一种扩展，相应的实现有<a title="https://github.com/webpack-contrib/css-loader#modules" href="https://github.com/webpack-contrib/css-loader#modules">Webpack CSS-loader</a>（需开启 module 模式）、<a title="https://github.com/css-modules/postcss-modules" href="https://github.com/css-modules/postcss-modules">Postcss-module</a> ，以及适用于 TS 的 <a title="https://github.com/seek-oss/vanilla-extract" href="https://github.com/seek-oss/vanilla-extract">vanilla-extract</a>等。</p>
<p>在语义上，CSS Module 方案将每个 CSS 文件视为一个模块，默认其中的每一个 class 选择器都是模块级的（并且模块间可以互相导入导出）。在实现上，则是在编译时将每个 CSS module 内的所有 class name 替换成各不重复的随机字符串。对通过<code>import</code>语句引用了 CSS module 内的 class name 的 JS module，也会将其中的相关引用替换为这些随机字符串。</p>
<p>对大部分场景而言，该方案没有显著的缺点。 <a title="https://vue-loader.vuejs.org/guide/css-modules.html" href="https://vue-loader.vuejs.org/guide/css-modules.html">Vue-loader 也支持直接在 SFC 内写 CSS Module</a>。</p>
<h2 id="原子类">原子类</h2>
<p>原子类思想本身就是一种历史悠久（可追溯到本世纪一零年代以前）的 CSS 方法论，但在现代前端的组件化开发模式流行起来之前，<a title="https://www.zhihu.com/question/22110291" href="https://www.zhihu.com/question/22110291">原子类思想一直被视为反工程的</a>。而伴随着前端 JS 组件化开发的流行，原子类思想的应用条件开始成熟，因此被开发者重新审视。</p>
<p>原子类思想最为激进，是因为<a title="https://www.zhihu.com/question/337939566/answer/1105939733" href="https://www.zhihu.com/question/337939566/answer/1105939733">它注意到传统的“在 CSS 层面进行抽象”的必要性是十分可疑的</a>。这和 CSS-in-JS 社区的思想有些接近，但做得更进一步：用户连 CSS 都不用写，直接在了 HTML 上堆叠类名即可——不但跳过了<a title="#%E4%BC%A0%E7%BB%9F%E5%81%9A%E6%B3%95%E5%8F%8A%E5%85%B6%E9%97%AE%E9%A2%98" href="#%E4%BC%A0%E7%BB%9F%E5%81%9A%E6%B3%95%E5%8F%8A%E5%85%B6%E9%97%AE%E9%A2%98">传统步骤</a>的前三步，连第四步都简化了。</p>
<p>现代的原子类解决方案包括：</p>
<ul>
<li><a title="https://tailwindcss.com/" href="https://tailwindcss.com/">Tailwind.css</a> ：原子类 CSS 框架的首个成功的主流范例</li>
<li><a title="https://windicss.org/guide/" href="https://windicss.org/guide/">windicss</a>（<a title="https://cn.windicss.org/" href="https://cn.windicss.org/">中文文档</a>同步）：tailwind 的上位替代。在兼容前者的基础上，大大减少了配置项和依赖，增强了易用性</li>
<li><a title="https://github.com/antfu/unocss" href="https://github.com/antfu/unocss">unocss</a>：一个元原子类框架，或可称为原子类引擎。该工具可用于生成原子类框架（包括 tailwind 和 windi），适用于需要高度定制原子类的场景</li>
</ul>
<p><a href="https://zhuanlan.zhihu.com/p/425814828">《重新构想原子化CSS》</a>一文中详细对比了这三者，并着重推广了 unocss。</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Babel 的组成]]></title>
            <guid>5ebeddbc65be473883b4e432ae67e4bf</guid>
            <pubDate>Thu, 16 Jul 2020 13:50:01 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="用于编译期作为devdependcies">用于编译期（作为<code>devDependcies</code>）</h1>
<p>以下组件用于代码的编译，生成可发布的产品代码。</p>
<ul>
<li><code>@babel/core</code>：必须。一个可编程使用的工具。它依序做如下事：
<ol>
<li>将代码/代码文件生成 AST（依赖于底层的<code>@babel/parser</code>。它也会借助 syntax plugins 的能力以识别各种各样的语法，如 TS、JSX 等）</li>
<li>遍历该 AST（依赖于底层的<code>@babel/traverse</code>），生成新 AST（生成规则由 transform plugins 提供。插件通常会使用<code>@babel/types</code>中提供的辅助 AST 生成的小工具 ）</li>
<li>将新的 AST 转化为代码（依赖于底层的<code>@babel/generator</code>）</li>
</ol>
</li>
<li><code>@babel/cli</code>：若想以命令行的方式使用<code>@babel/core</code>暴露的接口，可使用该工具（除此之外 Babel 还支持<a title="https://babeljs.io/en/setup" href="https://babeljs.io/en/setup">其它方式</a>）。除了以在命令行中给该工具传参，也可通过配置文件（JS 或 JSON）控制其行为</li>
<li>各 plugin：插件的用途见上文。插件的行为通常可配置。可根据目的对插件进行分类：
<ul>
<li>syntax plugins：单纯扩展<code>@babel/parser</code>对代码的解析能力</li>
<li>transform plugins：描述如何根据原 AST 生成新 AST。一个 transform plugin 可能只负责某一条 ECMAScript 新语法的转译，也可能负责整个语言（如 JSX、TypeScript）到 JS 的转译</li>
</ul>
</li>
<li>各 preset：一个 preset 是一批插件的集合，该集合的行为通常可配置。最常用的是<code>@babel/preset-env</code>，它是一个动态的集合，所包含的插件由目标编译环境智能决定</li>
</ul>
<p>需要注意，<a title="https://babeljs.io/docs/en/next/plugins#plugin-ordering" href="https://babeljs.io/docs/en/next/plugins#plugin-ordering">加载 plugins/presets 时的顺序</a>是有意义的，例如，syntax plugins 应当在 transform plugins 前被加载，否则无法顺利进行 AST 生成。</p>
<h1 id="用于运行时作为dependencies">用于运行时（作为<code>dependencies</code>）</h1>
<p>以下组件的定位是“库”（library）。它们的代码将全部或部分地被混入产品代码中以确保转译得到的产品代码能正确执行。注意：<strong>它们均不保证 Web API 能正常使用，而只保证 ES 语法/标准API 的正常运行。</strong></p>
<ul>
<li><code>@babel/runtime</code>：语法转译通常使得原语法的部分语义由编译期来到运行时，因此需要提供额外的运行时代码（称为 Helper。依赖底层的<code>@babel/helper</code>生成）。该库提供了<strong>可复用</strong>的 helper（含为实现 ES2015 的 generator 和 ES2017 的 async function 的语义而引入的<code>regenerator-runtime</code>），<strong>需要搭配插件<code>@babel/plugin-transform-runtime</code>使用</strong>，从而避免重复生成 helper。如果并不想复用 helper，该库可以不安装</li>
<li><code>@babel/runtime-corejs2</code>/<code>@babel/runtime-corejs3</code>：结合了<code>@babel/runtime</code>和<code>core-js</code>两个库的内容。<strong>需要搭配插件<code>@babel/plugin-transform-runtime</code>使用</strong>（配置了<code>corejs</code>选项），为前者提供 helper 和 polyfill。<a title="https://github.com/babel/babel/issues/10008" href="https://github.com/babel/babel/issues/10008">2和3的区别在于后者不污染全局</a></li>
<li><code>@babel/polyfill</code>（<mark>已废弃</mark>）：JS 的很多新特性并非是新语法，而是新对象、新函数、新方法，语法转译对此无能为力，因此需要提供 polyfill。该库引用<code>regenerator runtime</code>和<code>core-js</code>两个库，做的事其实就是将它们引入（<code>@babel/polyfill</code>总共就十几行代码），在 Babel 7.4.0 版本之后，应当直接安装这两个库，而不用安装<code>@babel/polyfill</code></li>
</ul>
<h1 id="附录babelpreset-env的智能插件加载和-polyfill-引入">附录：<code>@babel/preset-env</code>的智能插件加载<s>和 polyfill 引入</s></h1>
<p><code>@babel/preset-env</code>可以：</p>
<ul>
<li>根据设定的编译目标（<code>targets</code>）智能选择加载哪些插件进行语法转译。不设定，则转译任何 ES2015+ 的代码</li>
<li>以设定好的方式（<code>useBuiltIns</code>选项）往代码里插入<code>require</code> corejs 模块的语句（仅仅是<code>require</code>语句，并无任何 polyfill 的具体代码.因此你还是需要手动安装<code>core-js</code>）。具体见<a title="#%E4%BD%BF%E7%94%A8-babelpreset-env" href="#%E4%BD%BF%E7%94%A8-babelpreset-env">下文</a></li>
</ul>
<h1 id="附录插件babelplugin-transform-runtime的行为">附录：插件<a title="https://babeljs.io/docs/en/babel-plugin-transform-runtime" href="https://babeljs.io/docs/en/babel-plugin-transform-runtime"><code>@babel/plugin-transform-runtime</code></a>的行为</h1>
<p>该插件通过自动插入某些<code>require</code>语句，起到优化 Babel 生成的产品代码的作用，包括：</p>
<ul>
<li>在产品代码中复用<code>@babel/runtime</code>或<a title="https://babeljs.io/docs/en/babel-runtime-corejs2" href="https://babeljs.io/docs/en/babel-runtime-corejs2"><code>@babel/runtime-corejs2/3</code></a>（配置了<code>corejs</code>选项时）中的 helper 与 polyfill 代码（通过在各处生成<code>require("@babel/runtime/...</code>语句。因此必须安装<code>@babel/runtime</code>或<code>@babel/runtime-corejs2/3</code>作为dependency）</li>
<li>（可选）根据源代码（含 helper 中的代码）中对新 API 的实际使用情况，往代码里插入<code>require</code> corejs 相应模块的语句。具体见<a title="#%E4%BD%BF%E7%94%A8-babelplugin-transform-runtime" href="#%E4%BD%BF%E7%94%A8-babelplugin-transform-runtime">下文</a>)</li>
</ul>
<h1 id="附录polyfill-的处理">附录：polyfill 的处理</h1>
<h2 id="使用-babelpreset-env">使用 <code>@babel/preset-env</code></h2>
<p>与 polyfill 相关的选项包括：</p>
<ul>
<li><code>targets</code>：设定编译目标环境（含语法和 API）</li>
<li><code>useBuiltIns</code>：设置插入<code>require</code> corejs 语句的方式
<ul>
<li><code>"usage"</code>：<strong>根据新 API 在源码里的实际使用情况和编译目标环境（targets）</strong>，在代码中插入对相应 corejs 模块的<code>require</code>语句</li>
<li><code>"entry"</code>：<strong>根据编译目标环境（targets）</strong>，把源码中程序员手动写的<code>import core-js</code>之类的代码（一般出现在入口代码处）转成对相应 corejs 模块的<code>require</code>语句</li>
<li><code>false</code>（默认）：啥也不做。程序员应当手动写对 regenerator runtime 和core-js 两个库（或者 @babel/polyfill）的导入语句</li>
</ul>
</li>
<li><code>corejs</code>：告知该插件你正在使用 corejs 的何种版本（一般使用 corejs3，<a title="https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#what-changed-in-core-js3" href="https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#what-changed-in-core-js3">它具有更多的功能</a>）</li>
<li><code>exclude</code>和<code>include</code>：强制排除/加上对指定 corejs 模块的引入</li>
</ul>
<p>注意，<strong>无论如何设置，该插件<code>require</code>的 polyfill 代码都将污染全局的原型对象</strong>。</p>
<h2 id="使用-babelplugin-transform-runtime">使用 <code>@babel/plugin-transform-runtime</code></h2>
<p>通过该插件配置<code>corejs</code>选项，根据源代码中对新 API 的实际使用情况，插入对 corejs 相应模块的<code>require</code>语句。<strong>这将无效化<code>@babel/preset-env</code>中与 polyfill 相关的配置</strong>（因为 plugin 先于 preset 运行）。</p>
<p>该插件与上一个插件的区别是：</p>
<ul>
<li><s><a title="https://github.com/babel/babel/issues/10008" href="https://github.com/babel/babel/issues/10008">无法像<code>@babel/preset-env</code>一样额外结合目标环境按需引入。而是插入所有对“源码中使用的 ES3 之后的 API 的 polyfill“的 require 语句</a></s>（更新：<a title="https://github.com/babel/babel/pull/11572#issuecomment-785499971" href="https://github.com/babel/babel/pull/11572#issuecomment-785499971">已经可以了</a>）</li>
<li>引入的 polyfill 不会污染全局（例如不会试图往<code>String.prototype</code>上面加东西）。因此<strong>库或工具的作者</strong>总是应当使用这个插件来引入 polyfill</li>
</ul>
<h2 id="polyfill-providers">polyfill providers</h2>
<p><a title="https://github.com/babel/babel-polyfills" href="https://github.com/babel/babel-polyfills">polyfill providers</a> 是一系列插件的合称（在一个项目中，通常选用其中的一个插件）。这类插件根据用户代码的实际情况和设置好的目标环境，自动生成<code>require</code>语句，引入刚好必要的 polyfill 代码。</p>
<p>这类插件支持污染全局/不污染全局，支持按需加载（根据用户代码的实际情况 + 目标编译环境）/全部加载（根据目标编译环境） polyfill，还支持用户自定义 polyfill 的实现库（而不是限制为 corejs）。因此可以取代<code>@babel/preset-env</code>和<code>@babel/plugin-transform-runtime</code>在 polyfill 方面的功能。</p>
<h1 id="附录最佳实践">附录：最佳实践</h1>
<p>综上，可以得出配置 Babel 的最佳实践：</p>
<h2 id="业务项目开发">业务项目开发</h2>
<ol>
<li>安装<code>core-js@3</code>和@<code>babel/runtime</code></li>
<li>安装并使用<code>@babel/preset-env</code>。设置<code>useBuiltIns: 'usage'</code>选项</li>
<li>安装并使用<code>@babel/plugin-transform-runtime</code>，但不要配置其<code>corejs</code>选项</li>
</ol>
<h2 id="类库开发">类库开发</h2>
<ol>
<li>安装<code>@babel/runtime-corejs3</code></li>
<li>安装并使用<code>@babel/plugin-transform-runtime</code>，配置其<code>corejs: 3</code>选项</li>
</ol>
<h1 id="附录其它实用工具">附录：其它实用工具</h1>
<ul>
<li><code>@babel/register</code>：在运行时增强 Node 中<code>require</code>函数的功能，使其在被调用时能够实时利用 Babel 转译目标代码。对<code>require('@babel/register')</code>对调用必须在程序的最前面。该功能通常用于在开发过程中提供方便</li>
<li><code>@babel/node</code>：一个 Node.js CLI wrapper。使得通过命令行调用 Node.js 时令 Node.js 自动使用 babel 转译其加载的每个文件</li>
<li><code>@babel/code-frame</code>：一个为文本生成带标记（例如行号，行指示符号，列指示符号等）的文本的工具。通常用于生成错误提示</li>
<li><code>@babel/template</code>：待补充</li>
</ul>
<h1 id="附录tsc-vs-babel">附录：tsc vs. Babel</h1>
<p>对于使用了 TypeScript 的项目，我们有两个选择：</p>
<ul>
<li>使用 tsc 编译源码</li>
<li>使用 <a title="https://babeljs.io/docs/en/babel-preset-typescript" href="https://babeljs.io/docs/en/babel-preset-typescript">babel with typescript preset</a> 编译源码</li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th>使用 tsc</th>
<th>使用 babel with ts preset</th>
<th>评价</th>
</tr>
</thead>
<tbody>
<tr>
<td>ES 语法</td>
<td>TS 通常只吸收成熟的 ES 语法</td>
<td>可以使用最前沿的 ES 语法</td>
<td>建议只使用成熟语法。<strong>打平</strong></td>
</tr>
<tr>
<td>TS 语法</td>
<td>100% 支持</td>
<td>极个别的 TS 语法不支持</td>
<td>基本<strong>打平</strong></td>
</tr>
<tr>
<td>polyfill 支持</td>
<td>无法自动按需加载</td>
<td>preset-env 支持按需加载</td>
<td>除非项目无须 polyfill，否则<strong>babel 胜</strong></td>
</tr>
<tr>
<td>编译速度（不含 type check 时间）</td>
<td><a title="https://github.com/Brooooooklyn/swc-node#benchmark" href="https://github.com/Brooooooklyn/swc-node#benchmark">tsc 更占优</a></td>
<td>-</td>
<td><strong>tsc 胜</strong></td>
</tr>
<tr>
<td>编译产物（不含 polyfill）的体积与运行时效率</td>
<td><a title="https://github.com/ReactiveX/rxjs/pull/2093" href="https://github.com/ReactiveX/rxjs/pull/2093">tsc 更占优</a></td>
<td>-</td>
<td><strong>tsc 胜</strong></td>
</tr>
</tbody>
</table>
<p>使用 babel with typescript preset 时的注意事项：</p>
<ul>
<li>需要在构建流程中额外配置 type check 过程。例如与 Webpack 一同使用时，可使用 <a title="https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#readme" href="https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#readme">fork-ts-checker-webpack-plugin</a>；或单独使用 tsc 的 type check 功能</li>
<li>注意是否要开启 tsconfig.json 中的<a title="https://www.typescriptlang.org/tsconfig#isolatedModules" href="https://www.typescriptlang.org/tsconfig#isolatedModules"><code>isolatedModules</code></a>选项。开启该选项时，tsc type checker 将禁止你使用一些“可能引起非 tsc 的编译器（如 Babel）的错误行为”的语言特性，但其实你的编译器（如 Babel）可能是支持这些语言特性的</li>
<li>若需要生成<a>.d.ts 文件</a>（通常作为库作者），则需额外使用 tsc。</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Webpack 的概念]]></title>
            <guid>58770a1133c340aa8feea3a1c7dac994</guid>
            <pubDate>Fri, 19 Jun 2020 04:53:00 GMT</pubDate>
            <content:encoded><![CDATA[<nav class="table-of-contents"><ul><li><a href="#基本组件">基本组件</a></li><li><a href="#基本概念">基本概念</a><ul><li><a href="#module">module</a><ul><li><a href="#处理以特殊形式进行导出的模块">处理以特殊形式进行导出的模块</a><ul><li><a href="#以全局变量形式导出的模块">以全局变量形式导出的模块</a></li><li><a href="#以混合模块化语法导出的模块">以混合模块化语法导出的模块</a></li><li><a href="#以非上述方式导出的模块">以非上述方式导出的模块</a></li></ul></li><li><a href="#处理使用了全局变量的消费者模块">处理使用了全局变量的消费者模块</a></li><li><a href="#不打包某些被使用的模块">不打包某些被使用的模块</a></li></ul></li><li><a href="#chunk">chunk</a><ul><li><a href="#为什么有时需要多个-entry-point">为什么有时需要多个 entry point</a></li><li><a href="#为什么会有-non-initial-chunk">为什么会有 non-initial chunk</a></li></ul></li><li><a href="#webpack-runtime">webpack runtime</a></li><li><a href="#环境变量">环境变量</a><ul><li><a href="#给-webpack-配置文件提供环境变量">给 Webpack 配置文件提供环境变量</a></li><li><a href="#给项目代码提供环境变量">给项目代码提供环境变量</a></li></ul></li><li><a href="#tree-shaking">Tree shaking</a></li></ul></li><li><a href="#作为库作者">作为库作者</a></li></ul></nav><h1 id="基本组件">基本组件</h1>
<ul>
<li><code>webpack</code>：核心实现，并提供了 API</li>
<li><code>webpack-cli</code>：在命令行使用上述 API 的命令行工具。可以传参使用，也可以使用 webpack 配置文件</li>
</ul>
<h1 id="基本概念">基本概念</h1>
<h2 id="module">module</h2>
<p>可以被某代码语句（例子见下述列表）引入的东西，会被 Webpack 视为 module。</p>
<ul>
<li>ES 的 <code>import</code>/<code>import()</code> 语句</li>
<li>CJS 的<code>require</code>语句</li>
<li>AMD 的<code>define</code>和<code>require</code>语句</li>
<li>css/sass/less 的<code>@import</code>语句、<code>url(...)</code></li>
<li>HTML 的 <code>&lt;img src=""&gt;</code></li>
<li>…</li>
</ul>
<p>可见，JS 模块文件、CSS/SASS/LESS 文件、图片文件等均可作为 Webpack module。而且不同类型的 module 可以互相引用（例如 JS module 中可以引入 SASS 文件、图片文件等）。</p>
<p>Webpack 使用不同的 loader（默认自带 ES Module/CJS/AMD JavaScript loader 和 JSON loader） 解析不同类型的 Webpack module，并将其转化为某种其它形式（例如将图片转换为 base64 字符串/相对 URL 等）。</p>
<p>各 module 间通过上述语句建立起依赖关系。<a title="https://webpack.js.org/guides/dependency-management/#require-with-expression" href="https://webpack.js.org/guides/dependency-management/#require-with-expression">如果在一个模块中， Webpack 无法通过<code>require</code>语句得知该模块具体需引入哪个其它模块（例如<code>require</code>的参数是在运行时求得的），则所有可能引入的模块都将被视为该模块的依赖</a>。这背后的机制是 webpack 自动生成了一个 require.context（一个编译期信息。可以由程序员手动在编译期生成）。</p>
<p>各 module 以及它们互相间的依赖关系组成了 Dependency Graph。 Webpack 总是从一个作为 entry point 的 module 开始，分析、建立起一个 Dependency Graph的。在一次构建中可能建立起多个 Dependency Graph。</p>
<h3 id="处理以特殊形式进行导出的模块">处理以特殊形式进行导出的模块</h3>
<p>本节介绍如何用 webpack 导入前 ESM 时代的可复用代码，这些代码通常以不标准的方式进行导出。但我们没必要对它们的源码进行修改，因为 webpack 提供了如下解决方案：</p>
<h4 id="以全局变量形式导出的模块">以全局变量形式导出的模块</h4>
<p>有一些老旧的库并未使用任何模块化语法进行导出，而是把自己注册为一个全局变量。这种情况<a title="https://webpack.js.org/guides/shimming/#global-exports" href="https://webpack.js.org/guides/shimming/#global-exports">可使用 exports-loader</a> 将其纳入正常的 webpack module 体系内，而无需对库源码做任何改动。</p>
<h4 id="以混合模块化语法导出的模块">以混合模块化语法导出的模块</h4>
<p>一些老旧的库使用了混合的模块化导出语法，例如先检查有无<code>define</code>函数（AMD），再检查有无<code>require</code>（CJS），最后以注册为全局变量为兜底。这种情况一般<a title="https://webpack.js.org/guides/shimming/#other-utilities" href="https://webpack.js.org/guides/shimming/#other-utilities">通过 imports-loader 的配置</a>，强制将其视为以 CJS 语法进行导出的模块。</p>
<h4 id="以非上述方式导出的模块">以非上述方式导出的模块</h4>
<p>在 noParse 选项中标记该库，将其原样打包进打包产物中。</p>
<h3 id="处理使用了全局变量的消费者模块">处理使用了全局变量的消费者模块</h3>
<p>有一些老旧模块通过全局变量使用它的依赖，而这些依赖现已有模块化导出的版本了。此时可<a title="https://webpack.js.org/guides/shimming/#shimming-globals" href="https://webpack.js.org/guides/shimming/#shimming-globals">使用 ProvidePlugin</a>，在不改动该模块源代码的前提下使源代码正常运作。</p>
<h3 id="不打包某些被使用的模块">不打包某些被使用的模块</h3>
<p>这称为模块的“外化（Externalize）”，常见于：</p>
<ul>
<li>库作者不希望将自己依赖的第三方模块打包进构建产物中（例如已经将这些第三方模块列在 package.json 中的<code>dependencies</code>或<code>peerDependencies</code>字段里了）</li>
<li>依赖的第三方模块是运行时环境的一部分（例如在浏览器里通过引入这些第三方模块的 script 生成了全局变量）</li>
</ul>
<p>这种情况下应<a title="https://webpack.js.org/configuration/externals/" href="https://webpack.js.org/configuration/externals/">使用<code>externals</code>配置字段向 webpack 列举出此类依赖，还可列举出这些依赖可能使用的模块化语法</a>，以便 webpack 进行额外处理。注意，<code>externals</code>配置不侵入消费者模块的代码，这意味着在使用这些依赖的模块内，该 import 哪些符号就 import 哪些符号。</p>
<p>通过配置字段<a title="https://webpack.js.org/concepts/targets/" href="https://webpack.js.org/concepts/targets/"><code>target</code></a>，webpack 可以自动认得一些运行时环境变量，而无需手动列举（例如当 target 为 <code>node</code> 时，webpack 会自动外化<code>fs</code>、<code>path</code>等依赖）。</p>
<h2 id="chunk">chunk</h2>
<p>chunk 是 Webpack 打包构建后生成的代码文件。</p>
<p>一般来说，通过构建过程，<strong>源于一个</strong> entry point 的一个或多个（当以数组形式指定一个 entry point 时） Dependency Graph 中的所有 modules 被<strong>打包进一个 chunk</strong>。但<strong>有时可能会被打包为多个 chunk</strong>，此时称这些 chunk 组成一个 chunks group。一个 chunks group 中，必有一个 initial chunk 以及任意个 non-initial chunk（或称为 on-demand chunk）。后者的加载由前者触发。</p>
<h3 id="为什么有时需要多个-entry-point">为什么有时需要多个 entry point</h3>
<p>可能的场景如下：</p>
<ul>
<li>这是个多页应用，具有多个入口</li>
<li><a title="https://webpack.js.org/guides/shimming/#loading-polyfills" href="https://webpack.js.org/guides/shimming/#loading-polyfills">将所有 polyfill 代码置于一个额外的 entry point， 以打包出一个 polyfill chunk，供按需加载</a></li>
</ul>
<p>注意，用一个数组作为 entry point，并不能称“该项目具有多个 entry point”。这么做的例子见<a title="https://webpack.js.org/guides/entry-advanced/" href="https://webpack.js.org/guides/entry-advanced/">此处</a>。</p>
<h3 id="为什么会有-non-initial-chunk">为什么会有 non-initial chunk</h3>
<p>可能的场景如下：</p>
<ul>
<li>如果这是个多页应用，则可能需要从多个 Dependency Graph 中提取出相同的 module 来生成 common chunk，以供其它 chunk 复用，避免相同的 module 被重复构建到 chunk 里</li>
<li>代码中使用了  <a>dynamic import 语句</a>。 这种情况下 non-initial chunk 是懒加载的</li>
<li><a title="https://webpack.js.org/guides/caching/#extracting-boilerplate" href="https://webpack.js.org/guides/caching/#extracting-boilerplate">将一些更新频率很低的代码（例如第三方库）抽成一个单独的 chunk，以便利用浏览器缓存</a></li>
</ul>
<p>其中，1、3 两点基本上依赖于插件 <a title="https://webpack.js.org/plugins/split-chunks-plugin/" href="https://webpack.js.org/plugins/split-chunks-plugin/">SplitChunksPlugin</a>。该插件提供了各种划分 chunk 的方法。</p>
<p>注意：懒加载（按需加载）的 chunk 不一定是 non-initial chunk，non-initial chunk 也不一定是懒加载（按需加载）的。</p>
<h2 id="webpack-runtime">webpack runtime</h2>
<p>作为构建过程的产物的代码中，额外存在一个由 Webpack 提供的runtime chunk（一个 chunk group 默认产生一个 runtime chunk，它直接被插入到 HTML 的 script 里作为内联脚本了。但可以<a title="https://webpack.js.org/guides/caching/#extracting-boilerplate" href="https://webpack.js.org/guides/caching/#extracting-boilerplate">通过配置，使得所有 chunk group 共用同一个 runtime chunk</a>）。其中的代码负责进行所有其它 chunk 的加载和内部各 module 的连接。其中会用到一种称为 manifest 的信息，它包含了各模块间关系的描述。manifest 信息可以被 webpack plugin 加以利用。</p>
<p>针对不同的目标环境（浏览器/node/electron等），Webpack 生成的 runtime 代码也不同。目标环境由配置字段<code>target</code>控制。</p>
<h2 id="环境变量">环境变量</h2>
<h3 id="给-webpack-配置文件提供环境变量">给 Webpack 配置文件提供环境变量</h3>
<p>可以<a title="https://webpack.js.org/guides/environment-variables/" href="https://webpack.js.org/guides/environment-variables/">在 Webpack 的配置文件中</a>额外使用 Webpack CLI（而非操作系统或 Node）提供的环境变量（<code>--env</code>），以动态地选择配置。当然，也能使用操作系统或 Node 提供的环境变量。</p>
<h3 id="给项目代码提供环境变量">给项目代码提供环境变量</h3>
<p>使用 DefinePlugin 插件可实现该功能。在项目中使用<code>process.env.xxx</code>来使用定义好的环境变量。注意，此处的<code>process.env</code>和 NodeJS 的 <code>process.env</code> 没有任何关系，即，和操作系统或是 Node 提供的环境变量无关。<code>process.env.xxx</code>只是会被进行单纯的字符串替换，因此也不能对<code>process.env</code>使用解构语法等。</p>
<h2 id="tree-shaking">Tree shaking</h2>
<p>Webpack 在 production mode 下的打包过程中会删掉所有模块中并未被实际使用到的符号背后的代码（这背后是 <a title="https://webpack.js.org/configuration/optimization/#optimizationusedexports" href="https://webpack.js.org/configuration/optimization/#optimizationusedexports">optimization.usedExports 配置</a>在起作用），但这要求所有模块均使用了 ESM 语法。因此使用 NPM 上的库时，应当尽量寻找使用了 ESM 的库；同时也不能让 babel 等转译工具对<code>import</code>语句进行转译（通常 babel 会将 ESM 语法转为 CJS）。</p>
<p>有时我们 import 一个模块并非是为了使用它导出的符号，而是为了触发副作用。如果不做任何配置，Webpack 会因为该模块被不存在被外部使用的符号而将其删除。因此：</p>
<ul>
<li>如果你是库作者，不希望自己的库中的某些模块在用户编译时被 webpack 消除，可以<a title="https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free" href="https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free">在 package.json 中用<code>sideEffects</code>标记其中有副作用的模块</a>（可参考<a title="https://zhuanlan.zhihu.com/p/40052192" href="https://zhuanlan.zhihu.com/p/40052192">这篇文章</a>）</li>
<li>如果你是普通用户，可以通过配置 <a title="https://webpack.js.org/configuration/module/#rulesideeffects" href="https://webpack.js.org/configuration/module/#rulesideeffects">sideEffect module rule</a> 来告知 Webpack 哪些代码模块是有副作用的</li>
</ul>
<p>被标注为有副作用对模块中可能还是存在着纯函数的，这些纯函数可以被安全删除。使用<code>/*#__PURE__*/ </code>标记出纯函数。可以在函数的声明前使用它，也可<a title="https://webpack.js.org/guides/tree-shaking/#mark-a-function-call-as-side-effect-free" href="https://webpack.js.org/guides/tree-shaking/#mark-a-function-call-as-side-effect-free">在函数的调用前使用它</a>。</p>
<p>除此之外，我们还需要注意：</p>
<ul>
<li>Babel 等转译工具可能将看上去没有副作用的代码转译出副作用，从而导致 tree-shaking 失败。<a title="https://zhuanlan.zhihu.com/p/32831172" href="https://zhuanlan.zhihu.com/p/32831172">如果对ES6语义特性要求不是特别严格，可以开启babel的loose模式；并且<strong>确保转译发生在打包之后</strong></a></li>
<li>如果你是库作者，且库中难以避免地产生各种副作用代码，可以将功能函数或者组件，打包成单独的文件或目录，以便于用户可以通过目录去加载。如有条件，也可为自己的库开发单独的webpack-loader（或者类似于 babel-lodash-plugin 之类的插件），便于用户按需加载</li>
<li>如果你是库作者，记得提供</li>
</ul>
<h1 id="作为库作者">作为库作者</h1>
<p>如果你的代码需要由 Webpack 打包后进行分发供其他开发者使用，则需要注意以下事项：</p>
<ul>
<li>不要把自己使用的依赖库打包至构建产物中（见上文关于“<a title="#%E4%B8%8D%E6%89%93%E5%8C%85%E6%9F%90%E4%BA%9B%E8%A2%AB%E4%BD%BF%E7%94%A8%E7%9A%84%E6%A8%A1%E5%9D%97" href="#%E4%B8%8D%E6%89%93%E5%8C%85%E6%9F%90%E4%BA%9B%E8%A2%AB%E4%BD%BF%E7%94%A8%E7%9A%84%E6%A8%A1%E5%9D%97">不打包某些被使用的模块</a>”的段落）</li>
<li>为自己的构建产物指定所使用的模块化导出语法，以便其它开发者在各种环境中使用（然而<a title="https://github.com/webpack/webpack/issues/2933" href="https://github.com/webpack/webpack/issues/2933">直到 Webpack 5 ，官方功能都不支持构建为使用 ESM 导出语法的模块</a>）。<a title="https://webpack.js.org/configuration/output/#outputlibrarytarget" href="https://webpack.js.org/configuration/output/#outputlibrarytarget">使用<code>output.libraryTarget</code></a>及其相关配置即可</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Redux 及其生态的学习]]></title>
            <guid>70aa8a91732443b2bf7c1904a8d25c4c</guid>
            <pubDate>Sun, 09 Feb 2020 15:07:54 GMT</pubDate>
            <content:encoded><![CDATA[<nav class="table-of-contents"><ul><li><a href="#基本概念">基本概念</a><ul><li><a href="#action">Action</a></li><li><a href="#reducers">Reducers</a></li><li><a href="#store-与-state">Store 与 State</a></li><li><a href="#redux-扩展">Redux 扩展</a><ul><li><a href="#store-enhancer">Store Enhancer</a></li><li><a href="#中间件">中间件</a></li></ul></li><li><a href="#异步操作副作用">异步操作（副作用）</a><ul><li><a href="#action-creators-での副作用redux-thunk-redux-promise">action creators での副作用：redux-thunk/ redux-promise</a></li><li><a href="#reducer-での副作用redux-loop">reducer での副作用：redux-loop</a></li><li><a href="#额外抽象での副作用redux-observable">额外抽象での副作用：redux-observable</a></li><li><a href="#额外抽象での副作用redux-saga">额外抽象での副作用：redux-saga</a></li></ul></li></ul></li><li><a href="#与-react-共用">与 React 共用</a><ul><li><a href="#local-state-or-global-state">Local State Or Global State？</a></li><li><a href="#模式">模式</a><ul><li><a href="#一个组件两个部分">一个组件，两个部分</a></li><li><a href="#为组件树提供-store">为组件树提供 store</a></li></ul></li><li><a href="#react-redux">react-redux</a></li></ul></li><li><a href="#其它社区方案">其它社区方案</a><ul><li><a href="#ducks-modular-redux-规范">ducks-modular-redux 规范</a></li><li><a href="#高级封装方案">高级封装方案</a></li></ul></li></ul></nav><h1 id="基本概念">基本概念</h1>
<p>使用 Redux 时，程序员被要求通过向 Store 单例对象发送 action 对象（携带着 payload）来触发一个root reducer 函数（一个纯函数）的调用，其返回值将作为新的、全局唯一的、只读的 state 对象以供消费（通常用来渲染 UI）。</p>
<h2 id="action">Action</h2>
<p>action 对象应当是一个普通的 JS Object。一个 action 对象除了应当具有<code>type</code>属性外，没有其他约束（参阅<a title="https://redux.js.org/style-guide/style-guide#write-actions-using-the-flux-standard-action-convention" href="https://redux.js.org/style-guide/style-guide#write-actions-using-the-flux-standard-action-convention">官方推荐的 action 对象结构</a>）。通常会使用 action 工厂函数（称为 Action Creators）来创建 action 对象。<strong>Action Creators 中通常会出现副作用</strong>。<code>bindActionCreators(actionCreators, dispatch)</code>可以包装一个或多个 action creator，使它（们）在返回 action 对象后自动调用<code>dispatch</code>（通常是 store 实例的 <code>dispatch</code>方法）。</p>
<p><a title="https://redux.js.org/style-guide/style-guide#model-actions-as-events-not-setters" href="https://redux.js.org/style-guide/style-guide#model-actions-as-events-not-setters">action 应当被视为整个应用中的事件对象</a>而不是某种操作的触发。持这种视角，可以避免使用太多种类的 action（正如一个系统中，通常 事件的种类比 操作的种类少得多）、<a title="https://redux.js.org/style-guide/style-guide#avoid-dispatching-many-actions-sequentially" href="https://redux.js.org/style-guide/style-guide#avoid-dispatching-many-actions-sequentially">避免 dispatch 太多的 action</a>（从而触发太多的 UI 渲染），也有利于程序员<a title="https://redux.js.org/style-guide/style-guide#write-meaningful-action-names" href="https://redux.js.org/style-guide/style-guide#write-meaningful-action-names">为 action 的 type 起名字</a>（另见<a title="https://redux.js.org/style-guide/style-guide#write-action-types-as-domain-eventname" href="https://redux.js.org/style-guide/style-guide#write-action-types-as-domain-eventname">官方推荐的起名方式</a>）。而且也利于<a title="https://redux.js.org/style-guide/style-guide#allow-many-reducers-to-respond-to-the-same-action" href="https://redux.js.org/style-guide/style-guide#allow-many-reducers-to-respond-to-the-same-action">使用多个 reducer 处理同一个 action</a></p>
<h2 id="reducers">Reducers</h2>
<p>reducers 函数在 action 对象被发送时被触发，并根据当前的 state 对象和传入的 action 对象，计算得出一个新的 state 对象（必须是一个 plain js object）。该类函数的签名如下：</p>
<div><pre class="hljs"><code>(previousState, action) =&gt; newState</code></pre></div>
<p>reducers 函数<strong>必须是纯的</strong>。</p>
<p>典型模式是一个入口 reducer （称为 root reducer） 根据 action 的 type，组合调用不同的 reducer 来生成并返回新的 state 对象。root reducer 通常由各个子 reducer （它们只负责处理 state 对象的某一个部分）经由 Redux 提供的函数<code>combineReducers</code>组合而成。该函数的功能仅仅是令被组合的各 reducer 被逐一调用，并将调用后的值作为 state 的相应 key 的值而已。</p>
<p>其最佳实践包括：</p>
<ul>
<li><a title="https://redux.js.org/style-guide/style-guide#put-as-much-logic-as-possible-in-reducers" href="https://redux.js.org/style-guide/style-guide#put-as-much-logic-as-possible-in-reducers">reducer 内的逻辑越多越好</a></li>
<li><a title="https://redux.js.org/style-guide/style-guide#reducers-should-own-the-state-shape" href="https://redux.js.org/style-guide/style-guide#reducers-should-own-the-state-shape">reducer 代码要清晰体现 state 的结构</a></li>
<li><a title="https://redux.js.org/style-guide/style-guide#treat-reducers-as-state-machines" href="https://redux.js.org/style-guide/style-guide#treat-reducers-as-state-machines">应当用状态机的视角看待 reduer</a>，意思是，state 的改变应当只由特定条件触发，而不是由 action 触发（当然，特定条件的触发是由 action 带来的）</li>
</ul>
<h2 id="store-与-state">Store 与 State</h2>
<p>使用函数<code>createStore(rootReducer, preloadedState, storeEnhancer)</code>创建一个 Store 对象。一个应用使用唯一的 Store 对象。它负责：</p>
<ul>
<li>初始化并保存 state 对象（一个 plain js object）作为应用的<strong>唯一数据源</strong>，并提供一个只读版本（<code>store.getState()</code>)</li>
<li>接收 action 对象（<code>dispatch(action)</code>方法），然后调用 root reducer（root reducer 是 <code>createStore</code>函数的第一个参数）。root reducer 可以通过<code>store.replaceReducer</code>随时更换</li>
<li>注册 state 改变时的回调函数（<code>store.subscribe(listener)</code>）。回调函数不接收任何参数。一般用于启动一次渲染过程</li>
</ul>
<p>如何设计 state 对象的结构，是开发 Redux 应用的首要且核心的问题。它通常呈现为一个状态树：</p>
<ul>
<li>一个状态树节点只由一个模块来负责更新（但对所有模块可读）</li>
<li>避免冗余数据，以避免维护数据一致性。可以考虑使用 reselect 这样的 store data getter 库来避免对“去规范化（denormalization）”的需求。但有时冗余数据也不可避免，例如应用基于路由驱动时，当前的路由信息通常会被保存在 URL 和 store 中，此时需要诸如 react-router-redux 这样的库来保持两处信息的同步</li>
<li><a title="https://redux.js.org/style-guide/style-guide#normalize-complex-nested-relational-state" href="https://redux.js.org/style-guide/style-guide#normalize-complex-nested-relational-state">状态树的结构不要太深，尽量保持扁平</a></li>
</ul>
<h2 id="redux-扩展">Redux 扩展</h2>
<h3 id="store-enhancer">Store Enhancer</h3>
<p><code>createStore</code>函数的第三个参数是一个 Store Enhancer 。Store Enhancer 是一个函数，它在<code>createStore</code>中是这样被使用的：</p>
<div><pre class="hljs"><code><span class="hljs-keyword">function</span> <span class="hljs-title function_">createStore</span>(<span class="hljs-params">reducer, preloadedState, enhancer</span>) {
<span class="hljs-comment">// ...  </span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> enhancer !== <span class="hljs-string">'undefined'</span>) {
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> enhancer !== <span class="hljs-string">'function'</span>) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">'Expected the enhancer to be a function.'</span>)
        }
        <span class="hljs-keyword">return</span> <span class="hljs-title function_">enhancer</span>(createStore)(reducer, preloadedState)
    }
<span class="hljs-comment">//...</span>
}</code></pre></div>
<p>实现一个 Store Enhancer 的目的是为了重新实现（包装） store 对象的方法（ dispatch、 subscribe、 getState）或增加新方法。</p>
<h3 id="中间件">中间件</h3>
<p>“中间件”指的是一类函数，该类函数（们）被用于通过 enhancer 机制 扩展 dispatch 函数的功能。多个中间件实际构成了一个处理 action 对象 的管道， action 对象被这个管道中所有中间件依次处理过之后，才<em>有机会</em>（action 对象可能被某个中间件吞掉而不交给下一个中间件）被 reducer 处理。</p>
<p>使用<code>applyMiddleware</code>函数来得到 enhancer。该函数的实现如下：</p>
<div><pre class="hljs"><code>export <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">applyMiddleware</span><span class="hljs-params">(<span class="hljs-rest_arg">...middlewares</span>)</span> {
  <span class="hljs-keyword">return</span> createStore =&gt; (...args) =&gt; {
    <span class="hljs-keyword">const</span> store = createStore(...args)
    let dispatch = () =&gt; {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Error(
        <span class="hljs-string">'Dispatching while constructing your middleware is not allowed. '</span> +
          <span class="hljs-string">'Other middleware would not be applied to this dispatch.'</span>
      )
    }


    <span class="hljs-keyword">const</span> middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) =&gt; dispatch(...args)
    }
    <span class="hljs-keyword">const</span> chain = middlewares.map(middleware =&gt; middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)


    <span class="hljs-keyword">return</span> {
      ...store,
      dispatch
    }
  }
}</code></pre></div>
<h2 id="异步操作副作用">异步操作（副作用）</h2>
<p><strong>副作用是阻碍代码清晰、以及无法回溯的第一道障碍，因此必须将其隔离在中间件内</strong>。 处理异步操作（即用于隔离副作用）的中间件一般要“吞噬”掉某些类型的 action 对象，这样的 action 对象不会交还给中间件管道；随后中间件会根据异步I/O的结果产生新的 action 对象，并使用 dispatch 函数派发。</p>
<p>在 Redux 的单向数据流中，什么时机插入异步操作（副作用）？ 通过定制化 Store Enhancer，可以在 action 派发路径上任何一个位置插入异步操作（隔离副作用）， 甚至作为纯函数的 reducer 都可以帮助实现异步操作（但别这么做）。</p>
<p>不同的解决方案提供了不同的解答。在选择一个异步解决方案时，主要应考虑上述问题。社区方案有从 redux-thunk 到 redux-saga，再到基于此的一套高阶封装框架。我们会在后面介绍。</p>
<h3 id="action-creators-での副作用redux-thunk-redux-promise">action creators での副作用：redux-thunk/ redux-promise</h3>
<p>redux-thunk  只有十几行代码：</p>
<div><pre class="hljs"><code><span class="hljs-keyword">function</span> <span class="hljs-title function_">createThunkMiddleware</span>(<span class="hljs-params">extraArgument</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">{ dispatch, getState }</span>) =&gt;</span> <span class="hljs-function">(<span class="hljs-params">next</span>) =&gt;</span> <span class="hljs-function">(<span class="hljs-params">action</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> action === <span class="hljs-string">'function'</span>) {
            <span class="hljs-keyword">return</span> <span class="hljs-title function_">action</span>(dispatch, getState, extraArgument);
    }


      <span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>(action);
    };
}


<span class="hljs-keyword">const</span> thunk = <span class="hljs-title function_">createThunkMiddleware</span>();
thunk.<span class="hljs-property">withExtraArgument</span> = createThunkMiddleware;


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> thunk;</code></pre></div>
<p>原理是 判断每个经过它的 action ：如果是 function 类型（称其为thunk），就调用这个 function （并传入 dispatch 和 getState 及 extraArgument 为参数）然后吞掉它。这个 function 通常包含各种副作用，如异步I/O，并在异步I/O获得结果后调用 dispatch。既然 action 是 function，则 action creators 就是作为函数工厂的高阶函数。</p>
<p>为什么能 dispatch 一个 thunk function 作为 action ，action 不应该是 plain JS object 么？实际上，只要将 thunk function 视为“延时求值的数据”（实际上这就是"thunk"这一术语的原意），那么 “dispatch 一个 thunk function” 和  “dispatch 一个 plain JS object”在形式上就是统一的。</p>
<p>将上文所述的 thunk function 替换成 promise 对象，就是 redux-promise 库做的事。此处不再赘述。而为了克服它无法应对“<a title="https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/optimistic-updates.md" href="https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/optimistic-updates.md">乐观更新</a>”的缺点，另有 redux-promise-middleware 作为解决方案。</p>
<h3 id="reducer-での副作用redux-loop">reducer での副作用：redux-loop</h3>
<p>（暂略）</p>
<h3 id="额外抽象での副作用redux-observable">额外抽象での副作用：redux-observable</h3>
<p>该方案的要点如下：</p>
<ul>
<li>以（全局唯一的）流的形式提供 actions 让 reducer 加以处理</li>
<li>用  rxjs 的  creation operator 触发副作用、产生输入，而   action creators  保持纯函数</li>
</ul>
<p>该库提供的核心抽象是 Epics，即这么一类用户定义的函数：接收 stream of actions（已经经由 reducer 处理过了）和 stream of store states，返回另一个 用于 dispatch  的 stream of actions（显然，该库把 redux 的 dispatch 用于触发另一个Epic，而不是触发reducer 更新 state）。其中在管道上可调用 creation operator（例如<code>ajax</code>、<code>delay</code>等） ，从而把 副作用置于 Epics  中。</p>
<p>多个Epics 可以通过<code>combineEpics</code>加以组合，每个 epic 通过<code>filter</code>操作符滤出自己关心的 action 加以响应。可通过组合得到一个 rootEpics，并通过 EpicMiddleware 的 run 方法挂载。</p>
<h3 id="额外抽象での副作用redux-saga">额外抽象での副作用：redux-saga</h3>
<p>redux-sage 提供一种称为 Saga 的抽象，用来实施副作用以及编排各种异步操作。Saga 是一个用户定义的 Generator 函数，其内部要 yield 出被称为 effects 的 plain js objects（使用  effect creator 生成 ）， redux-sage 以它们为指令进行相应的（同步或异步的、可能有副作用的）动作（包括对 store 的 dispatch），动作的结果将和控制流一起回到 Saga 中。综上，Saga 中的内容是同步、无副作用的、对 effect creator 的调用，还可以使用try…catch语句进行包裹（因为 JS 支持将错误抛回 generator 内）。虽然 Saga 中的代码是同步的，但 也支持以非阻塞/并发的方式触发副作用。</p>
<p>其它的技术细节包括：</p>
<ul>
<li>
<p>redux-sage 选择 generator function   而非 async function， 这是因为<a title="https://github.com/catcuts/catsblog/issues/46" href="https://github.com/catcuts/catsblog/issues/46">前者的功能比后者更强大</a>，适用于更多场景</p>
</li>
<li>
<p>redux-sage 复写了 store 的 dispatch 方法，所有给 store 的 action 都会交由它处理，因此能对它们进行监听而不漏过任何一个</p>
</li>
</ul>
<h1 id="与-react-共用">与 React 共用</h1>
<h2 id="local-state-or-global-state">Local State Or Global State？</h2>
<p>原教旨主义者会认为，应该使用 state 对象表述整个 UI 的状态，这样才能确保应用的所有状态变动都是可回溯的；而更现代一些的看法认为</p>
<ul>
<li>从数据角度看，对于只被一个组件使用的数据，无需将其放入全局 state 对象中，保存于那个组件实例之上即可（<a title="https://redux.js.org/style-guide/style-guide#avoid-putting-form-state-in-redux" href="https://redux.js.org/style-guide/style-guide#avoid-putting-form-state-in-redux">尤其是表单组件</a>）。可以用以下问题帮忙判断：
<ul>
<li><a title="https://overreacted.io/zh-hans/writing-resilient-components/#%E5%8E%9F%E5%88%99-4%EF%BC%9A%E9%9A%94%E7%A6%BB%E6%9C%AC%E5%9C%B0%E7%8A%B6%E6%80%81" href="https://overreacted.io/zh-hans/writing-resilient-components/#%E5%8E%9F%E5%88%99-4%EF%BC%9A%E9%9A%94%E7%A6%BB%E6%9C%AC%E5%9C%B0%E7%8A%B6%E6%80%81">“如果此组件呈现两次，交互是否应反映在另一个副本中？” 若答案为“否”，则为本地状态</a></li>
<li>“你是否希望这些状态能被随着组件自动销毁，而不是要程序员手动清理？”若答案为是，则为本地状态</li>
</ul>
</li>
<li>从组件角度上看，业务场景的组件适合绑定全局数据（不过这会导致阅读组件代码时需要频繁的在组件和全局状态内进行切换），业务无关的通用组件不适合绑定全局数据</li>
</ul>
<p>注意，本地状态固然不错，但维护过多的本地状态将导致该组件缺乏可复用性。因此，常常把组件拆分成<strong>容器组件</strong>和<strong>展示组件</strong>。我们马上谈论到这一模式。</p>
<h2 id="模式">模式</h2>
<h3 id="一个组件两个部分">一个组件，两个部分</h3>
<p>React 搭配 Redux 使用时，一个最小的功能单元应当用两个组件共同实现：<strong>容器组件</strong>和<strong>展示组件</strong>。后者位于前者的内层，除了负责根据前者（以 props 的形式）提供的数据渲染 DOM （以及在上面绑定事件回调函数以对store 进行 dispatch）以外什么都不做（即，是一个无状态组件），而前者负责从 store 中取出需要的数据并使用它们算出展示组件需要的数据。导出组件时，仅需导出外层的容器组件即可。</p>
<p>因此，展示组件不过是容器组件的附属物，可以视为是一种配置（模板）而已。而且<strong>具有很强的可复用性</strong>（UI 组件库正属此类）。</p>
<h3 id="为组件树提供-store">为组件树提供 store</h3>
<p>为组件树中的每个组件以 context 的形式提供那个全局唯一的 store。使用 React 的 Context 功能，在最顶层提供一个 context provider 即可实现。</p>
<h2 id="react-redux">react-redux</h2>
<p>react-redux 这个库为程序员提供了上一节的两个模式的现成实现。它们分别对应该库的以下两个 API：</p>
<ul>
<li><code>connect</code>函数：该函数将生成一个容器组件工厂。它需要程序员提供一个“选取 store 上的部分数据并映射为内层组件的 props”的函数（一般称为 mapStateToProps 函数），以及一个“将内层组件需要加以事件绑定的回调函数映射为内层组件的 props”（一般称为 mapDispatchToProps 函数）。得到容器组件工厂后，程序员给工厂函数传入展示组件即可得到相应的容器组件。出于性能考虑，<a title="https://redux.js.org/style-guide/style-guide#connect-more-components-to-read-data-from-the-store" href="https://redux.js.org/style-guide/style-guide#connect-more-components-to-read-data-from-the-store">容器组件的粒度宜细不宜粗，否则会触发太多的渲染过程</a>。</li>
<li>开箱即用的<code>&lt;Provider&gt;</code>组件</li>
</ul>
<p>connect 函数生成的容器组件工厂生成的容器组件实现了<code>shouldComponentUpdate</code>方法，策略是浅比对 old props 和 new props 有无差异。</p>
<h1 id="其它社区方案">其它社区方案</h1>
<p>除了要解决 Redux 的异步问题，社区还想解决诸如模版代码多、代码文件分散、代码组织方式没有规范（有的按照组件进行组织，有的按照功能进行组织）等问题。</p>
<h2 id="ducks-modular-redux-规范">ducks-modular-redux 规范</h2>
<p>为了避免每次修改都要修改一堆文件和制定文件规范，推出了ducks-modular-redux规范，将每个子module的文件都放置到一个文件里，这样大大简化了日常开发中一些冗余工作。该规范有一些相应的实现。</p>
<p>具体可见 <a title="https://github.com/erikras/ducks-modular-redux" href="https://github.com/erikras/ducks-modular-redux">erikras/ducks-modular-redux</a></p>
<h2 id="高级封装方案">高级封装方案</h2>
<p><a title="https://blog.staleclosure.com/react-state-mangement/" href="https://blog.staleclosure.com/react-state-mangement/">https://blog.staleclosure.com/react-state-mangement/</a></p>
<p>后来社区涌现出一批基于 Redux 的高级封装方案，旨在提供一整套关于代码组织、代码简化、异步流程等问题的解决方案（但此类封装通常与 Redux 的原始哲学背道而驰）。</p>
<p>样板代码简化的思路基本上是一致的。我们发现绝大部分的业务 model 都满足如下性质：</p>
<div><pre class="hljs"><code><span class="hljs-keyword">const</span> model = <span class="hljs-title function_">createModel</span>({
  <span class="hljs-attr">name</span>: <span class="hljs-comment">// 全局的key</span>
  <span class="hljs-attr">state</span>:xxx, <span class="hljs-comment">// 业务状态</span>
  <span class="hljs-attr">reducers</span>:xxx, <span class="hljs-comment">// 同步的action</span>
  <span class="hljs-attr">effects</span>:xxxx, <span class="hljs-comment">// 异步的action</span>
  <span class="hljs-attr">computed</span>: xxx <span class="hljs-comment">// state的衍生数据</span>
}</code></pre></div>
<p>因此绝大部分框架的都采用了类似的定义，区别只在于语法和名称有所不同。以下是两个知名解决方案，它们十分类似，只是在异步方案上有所不同：</p>
<ul>
<li><a title="https://github.com/rematch/rematch" href="https://github.com/rematch/rematch">rematch/rematch</a></li>
<li><a title="https://github.com/sorrycc/blog/issues/1" href="https://github.com/sorrycc/blog/issues/1">《React + Redux 最佳实践》</a>这篇文章中介绍了 dva 这一框架的思想</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TypeScript 语言特性索引 ]]></title>
            <guid>d1d7e7acc95f44dbad40c774c77d6935</guid>
            <pubDate>Tue, 30 Jul 2019 15:08:04 GMT</pubDate>
            <content:encoded><![CDATA[<nav class="table-of-contents"><ul><li><a href="#增强的oop">增强的OOP</a></li><li><a href="#类型系统">类型系统</a></li><li><a href="#其它">其它</a></li></ul></nav><p>该索引对应 TypeScript 版本：4.4</p>
<p>官方 wiki 见<a title="https://www.typescriptlang.org/docs/handbook/release-notes/overview.html" href="https://www.typescriptlang.org/docs/handbook/release-notes/overview.html">https://www.typescriptlang.org/docs/handbook/release-notes/overview.html</a></p>
<h1 id="增强的oop">增强的OOP</h1>
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers" href="http://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers">属性访问权限</a>：<code> public</code> 、<code> private</code>   和  <code> protected</code>（含直接<a title="http://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties" href="http://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties">在构造器参数中设定访问权限并初始化属性的语法糖</a> ）。对构造器方法也适用。其中<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#which-should-i-use" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#which-should-i-use"><code>private</code>与ES标准提供的<code>#</code>语法存在一定区别</a></li>
<li>设置<a title="http://www.typescriptlang.org/docs/handbook/classes.html#readonly-modifier" href="http://www.typescriptlang.org/docs/handbook/classes.html#readonly-modifier">只读属性</a>。包括直接<a title="http://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties" href="http://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties">在构造器参数中设定只读的语法糖</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes" href="http://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes">抽象类和抽象方法</a>：<code>abstract  </code></li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#optional-class-properties" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#optional-class-properties">类的可选属性和方法</a></li>
<li>接口（<code>interface</code>）
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#implementing-an-interface" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#implementing-an-interface">供类实现（<code>implement</code>）</a>。注意：对实现接口的类，<a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes">类型检查将被施加在类的实例上而不是类上</a>。这使得若要约束类的构造器，需要额外定义构造器接口（<a title="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#newable" href="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#newable">newable 函数</a> ）</li>
<li>可以<a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#extending-interfaces" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#extending-interfaces">继承其它接口</a>或<a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#interfaces-extending-classes" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#interfaces-extending-classes">继承其它类</a>（使用<code>extends</code>关键字）</li>
</ul>
</li>
</ul>
<h1 id="类型系统">类型系统</h1>
<p>通过尽可能地施加约束， 在编译期发现进行检查，从而尽量避免运行时错误。TS 中所有的类型约束检查均不是靠在编译得到的 JS 代码里加入运行时类型检查代码而进行的</p>
<ul>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export">对模块中类型的导入（<code>import type</code>）导出（<code>export type</code>）</a></p>
</li>
<li>
<p>类型系统可用于约束任何值（以变量或字面量的形式均可）。值得说明的包括：</p>
<ul>
<li>函数
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/functions.html#typing-the-function" href="http://www.typescriptlang.org/docs/handbook/functions.html#typing-the-function">约束其参数和返回值</a>（包括<a title="http://www.typescriptlang.org/docs/handbook/functions.html#optional-and-default-parameters" href="http://www.typescriptlang.org/docs/handbook/functions.html#optional-and-default-parameters">可选参数、默认参数</a>以及<a title="http://www.typescriptlang.org/docs/handbook/functions.html#rest-parameters" href="http://www.typescriptlang.org/docs/handbook/functions.html#rest-parameters">剩余参数</a>）</li>
<li><a title="https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters" href="https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters">约束其<code>this</code></a></li>
<li>支持<a title="http://www.typescriptlang.org/docs/handbook/functions.html#overloads" href="http://www.typescriptlang.org/docs/handbook/functions.html#overloads">重载声明（overload，不是 override）</a>，但不支持重载实现</li>
</ul>
</li>
<li>类</li>
<li>接口</li>
</ul>
</li>
<li>
<p>可供使用的类型：</p>
<ul>
<li>内置的类型
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/basic-types.html#introduction" href="http://www.typescriptlang.org/docs/handbook/basic-types.html#introduction">JS 的原始数据类型</a>。其中null和undefined类型见<a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#nullable-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#nullable-types">此处的详细说明</a></li>
<li>void、any（“类型系统你不要检查了！反正全是等价的”）、never（<a title="https://www.zhihu.com/question/354601204" href="https://www.zhihu.com/question/354601204">“反正这个值按理来说是不可能被 reach 的，就叫它 never 类型吧，并让它不与其它任何类型等价。如果该值被类型系统拿去比较了（必不匹配），说明逻辑有错”</a>）、<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type">unkonwn</a>（<a title="https://www.zhihu.com/question/355283769/answer/944821419" href="https://www.zhihu.com/question/355283769/answer/944821419">“我此时不清楚值的具体类型，但我在之后会看情况钦点（断言）一个。类型系统仍然要检查其合法性”</a>）</li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol">unique symbol</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/enums.html" href="http://www.typescriptlang.org/docs/handbook/enums.html">枚举</a>：用于（在编译期或运行时）定义带类型信息的常量 （常量值可自动获得/程序员手动指定） 。关键字<code>enum </code>
<ul>
<li>数字枚举、字符串枚举、混合枚举</li>
<li>constant member（symbol 在编译期即可得到值） 和  computed member（symbol 在运行时得到值）</li>
<li><code>const enum</code>：symbol 仅存在于编译期（因此不能包含 computed member）</li>
</ul>
</li>
<li>一些预置的 <a title="https://www.typescriptlang.org/docs/handbook/utility-types.html#introduction" href="https://www.typescriptlang.org/docs/handbook/utility-types.html#introduction">utility types</a>（。让程序员进行类型操作，利用泛型机制得到新类型</li>
</ul>
</li>
<li>自定义类型
<ul>
<li><a title="https://basarat.gitbooks.io/typescript/content/docs/types/literal-types.html" href="https://basarat.gitbooks.io/typescript/content/docs/types/literal-types.html">literal types</a>：任一能在编译期得到值的字符串/数字/布尔值/对象/数组的字面量（表示一个值域有且仅有一个值的类型）</li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types">Template Literal Types</a>：可以用模版字符串的语法生成 string literl types（<a title="https://www.zhihu.com/question/418792736" href="https://www.zhihu.com/question/418792736">用法示例</a>）</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/basic-types.html#array" href="http://www.typescriptlang.org/docs/handbook/basic-types.html#array">typed array</a>、<a title="http://www.typescriptlang.org/docs/handbook/basic-types.html#tuple" href="http://www.typescriptlang.org/docs/handbook/basic-types.html#tuple">typed tuple</a>（支持<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#optional-elements-in-tuple-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#optional-elements-in-tuple-types">可选的成员</a>、<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#rest-elements-in-tuple-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#rest-elements-in-tuple-types">剩余成员</a>（<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#leadingmiddle-rest-elements-in-tuple-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#leadingmiddle-rest-elements-in-tuple-types">未必要在尾部</a>））。它们都可以<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#improvements-for-readonlyarray-and-readonly-tuples" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#improvements-for-readonlyarray-and-readonly-tuples">使用<code>readonly</code>修饰符</a>。typed tuple 还支持<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements">为成员加 label</a></li>
<li>class</li>
<li>interface：描述数据应有结构（shape）。两种定义方式：使用<code>interface</code>关键字声明语法、<a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#our-first-interface" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#our-first-interface">使用  inline   语法</a>
<ul>
<li>对象的 shape：
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks">Excess Property Checks</a> （该约束只对对象字面量有效，即  <a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#stricter-object-literal-assignment-checks" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#stricter-object-literal-assignment-checks">strict object literal checking</a> ）</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties">可选属性 （<code>?:</code>） </a>（全是可选属性的接口是一个<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html#weak-type-detection" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html#weak-type-detection">weak type</a>。该种类型的属性不能被全部遗漏）</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#readonly-properties" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#readonly-properties">只读属性 （<code>readOnly</code>）</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types">索引属性 （<code>[index:]</code>）</a>（ 用于约束属性名和属性值，其中属性名支持 number / string / symbol 类型。 可用于<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#implicit-index-signatures" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#implicit-index-signatures">规避 Excess Property Checks</a>。<code>index</code>可以换成任何你喜欢的字样）。一个 interface 中可有多个索引属性</li>
</ul>
</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#function-types" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#function-types">函数的 shape</a>： 包括 <a title="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#newable" href="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#newable">newable 函数</a>、<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#abstract-construct-signatures" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#abstract-construct-signatures">抽象构造函数</a>。 支持约束<a title="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#obvious-examples" href="https://basarat.gitbooks.io/typescript/content/docs/types/callable.html#obvious-examples">可选参数、剩余参数</a>（其中剩余参数支持<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#spread-expressions-with-tuple-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#spread-expressions-with-tuple-types">使用 typed tuple 作类型</a>，并且<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types">tuple 中还可以有泛型</a> ）；支持<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#specifying-the-type-of-this-for-functions" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#specifying-the-type-of-this-for-functions">约束 <code>this</code></a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/interfaces.html#hybrid-types" href="http://www.typescriptlang.org/docs/handbook/interfaces.html#hybrid-types">混合的 shape</a>（用于约束一个可调用对象）</li>
<li>声明接口时，可以重复声明同一个接口，TS 会<a title="http://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces" href="http://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces">合并其中重复的规则</a></li>
</ul>
</li>
<li><a title="https://basarat.gitbooks.io/typescript/docs/types/functions.html#declaring-functions" href="https://basarat.gitbooks.io/typescript/docs/types/functions.html#declaring-functions">函数签名（callable类型）</a>：允许脱离具体的函数的声明或表达式来定义一个函数签名。通常用于为函数变量、高阶函数的（函数）参数等施加类型检查</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#polymorphic-this-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#polymorphic-this-types">Polymorphic <code>this</code> types</a>： 用于一个父类方法，用关键字<code>this</code>表示其任意子类型</li>
<li>经类型运算得到的类型
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types">Union types（<code>|</code>）</a>：多个类型取“或”关系。补充：
<ul>
<li>一个技巧是连接多个<a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types">字符串字面量</a>或<a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types">数字字面量</a>，组成一个可取值集合以作为一个类型</li>
<li>如果被连接的多个类型**都具有一个同名的成员，且在各类型中，满足：<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#non-unit-types-as-union-discriminants" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#non-unit-types-as-union-discriminants">未被指定为一个泛型类型，且至少在一个类型中被指定为 literal type**（各类型的该成员的类型不必都一样）</a>，则自动构成 <a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions">Discriminated Unions</a> （此时该成员称为 tag ） 。在使用 discriminated unions 时，TS 编译期会实施 <a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking">Exhaustiveness checking</a>（这是一个 type gurad特性。见下文），以检查使用它的代码是否处理了所有的 type（<a title="https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html#exhaustive-checks" href="https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html#exhaustive-checks">示例</a>）。对于 tag，TS 会试图<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking">将被连接的多个类型中的 tag 组成一个 union type</a></li>
</ul>
</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types">Intersection Types（<code>&amp;</code>）</a>：多个类型取“且”关系</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types">index type query operator（关键字<code>keyof</code>）</a>：常接一泛型类型参数。<code>keyof T</code> is the <strong>union</strong> of known, public property names(string、<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#support-number-and-symbol-named-properties-with-keyof-and-mapped-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#support-number-and-symbol-named-properties-with-keyof-and-mapped-types">number or symbol</a>) of <code>T</code>。对于 intersection type，有<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-keyof-with-intersection-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-keyof-with-intersection-types"><code> keyof (A &amp; B)</code>  等价于<code> keyof A | keyof B</code></a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types">indexed access operator（<code>T[K]</code>，其中T是一个类型名，K是T的一个字段名）</a> ： 常接泛型类型参数。得到类型T的字段K的类型。实例：该<a title="https://zhuanlan.zhihu.com/p/39620591" href="https://zhuanlan.zhihu.com/p/39620591">文章</a>中”巧用查找类型“一节</li>
<li><a title="https://mariusschulz.com/blog/type-queries-and-typeof-in-typescript#typescripts-type-queries" href="https://mariusschulz.com/blog/type-queries-and-typeof-in-typescript#typescripts-type-queries">type query（关键字<code>typeof</code>）</a>： A type query obtains the type of an   identifier   or   property access expression   (that is, multiple identifiers connected by dots)</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types">mapped types（关键字<code>in</code>）</a>：根据一个已有的类型（可以且通常是泛型）映射得到一个新类型。可映射名字的类型为string、<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#support-number-and-symbol-named-properties-with-keyof-and-mapped-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#support-number-and-symbol-named-properties-with-keyof-and-mapped-types">number 或 symbol 类型</a> 的属性。对于 typed array 和 typed tuple，<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays">只能映射它们的数字属性</a>。做映射时可以使用<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers"><code>+</code>和<code>-</code>为映射后的类型上的成员添加/去除修饰符</a>（例如<code>readonly</code>、<code>?</code>等），其中<code>readonly</code>也<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-mapped-type-modifiers-and-readonly-arrays" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-mapped-type-modifiers-and-readonly-arrays">能在给 typed array 和 typed couple 做映射时使用</a>。映射时，<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types">可以使用<code>as</code>关键字将欲映射的类型先映射一次</a>（也就是说，总的来说你可以映射两次）</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types">conditional types</a>：用来表述非单一形式的类型。A conditional type selects one of two possible types（<code>X</code>、<code>Y</code>） based on a condition expressed as a type relationship test（形如<code>T extends U ? X : Y</code>）, either resolved to <code>X</code> or <code> Y</code>, or   deferred until  type system has enough information to conclude  that <code> T</code>  is always assignable to <code> U</code>。一个具体例子可见<a title="https://zhuanlan.zhihu.com/p/64446259" href="https://zhuanlan.zhihu.com/p/64446259">这篇文章</a>中的“条件类型”一节
<ul>
<li>如果<code>T</code>是个 Union Type，则<a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types">会被展开（ distributed   ）</a></li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types"><code>infer</code> keyword</a> says to TypeScript: “<a title="https://dev.to/miracleblue/how-2-typescript-serious-business-with-typescripts-infer-keyword-40i5" href="https://dev.to/miracleblue/how-2-typescript-serious-business-with-typescripts-infer-keyword-40i5">I want to take whatever TypeScript infers to be at this position and assign it to a name</a>”。仅能用于<code>extends</code>子句中</li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types">支持递归</a></li>
</ul>
</li>
</ul>
</li>
<li>由<a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases">type alias（关键字<code>type</code>）</a>定义的类型。该功能用于为类型取别名（尤其是经类型运算得来的类型）。别名不能用来 implement 或 extends。<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases">可以递归定义</a></li>
</ul>
</li>
</ul>
</li>
<li>
<p>class, interface, enum, 和 type alias 的作用域<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#local-type-declarations" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#local-type-declarations">最小可以是块级</a></p>
</li>
<li>
<p>类型之间有兼容关系。兼容的两个类型不受类型约束的限制。TS 大体上采用的是”结构等价“的类型兼容性判断策略</p>
<ul>
<li><a title="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#starting-out" href="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#starting-out">接口间的兼容</a>： The basic rule for TypeScript’s structural type system is that   x   is compatible with  <code> y</code>   if  <code> y</code>   has at least the same members as  <code> x</code>。若想采用”名字等价“的类型兼容性判断策略，workaround 见<a title="https://basarat.gitbooks.io/typescript/content/docs/tips/nominalTyping.html" href="https://basarat.gitbooks.io/typescript/content/docs/tips/nominalTyping.html">此处</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#comparing-two-functions" href="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#comparing-two-functions">函数类型之间的兼容</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#enums" href="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#enums">枚举与Number之间的兼容</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#classes" href="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#classes">无继承关系的类与类之间的兼容</a></li>
<li><a title="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#generics" href="http://www.typescriptlang.org/docs/handbook/type-compatibility.html#generics">使用了泛型时的兼容规则</a></li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#fixed-length-tuples" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#fixed-length-tuples">tuple 类型与 tuple 类型之间的兼容</a></li>
<li>存在 subtype 或 extends 关系的类型之间</li>
</ul>
</li>
<li>
<p>程序员钦点类型的方法</p>
<ul>
<li>
<p><a title="http://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions" href="http://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions"><strong>类型断言</strong>（<code>as</code>）</a>，允许程序员指定编译器在编译期将任何值（可以是变量或者字面量）钦点为某个指定类型，以覆盖 TS 类型推断的得到的类型或此前程序员指定的类型（<em>一旦这么做，类型安全就需要由程序员自己加以保证了</em>）。<em>该功能不是在运行时发生的 type casting</em>。 Basically, the assertion from type   S   to   T   succeeds if either   S   is a subtype of   T   or   T   is a subtype of   S。亦可凭空（即不基于 subtype 关系）进行类型断言，但不建议这么做。类型断言的其它形式包括：</p>
<ul>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator">Non-null assertion operator（<code>!</code>）</a>：它断言其左边的值不会是 null 或 undefined</li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions">Definite Assignment Assertions（<code>!</code>）</a>：对一个变量使用，可以告诉 TypeScript 它的值的类型一定不会是    null 或 undefined</li>
<li>对字面量值可使用<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions"><code>as const</code>进行断言</a>，断言为一个 literal type。对对象字面量，其上所有成员 readonly；对数组字面量，断言得到 readonly tuple</li>
</ul>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#control-flow-based-type-analysis" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#control-flow-based-type-analysis">利用控制流来收窄类型</a>（比类型断言更灵活）</p>
<ul>
<li>
<p><a title="http://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards" href="http://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards">type predicate（<code>is</code>）</a>：利用  user-defined type guards function  进行类型保证，支持对<code>this</code>使用（即<code>this is ...</code>）。 TS 编译器把该类函数被调用且返回值为 true 的代码块内，被传给该类函数的变量的类型一律视为<code>is</code>指定的结果。该语言特性可用来消除一段代码内反复出现的对同一类型的断言</p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions">assert function</a>：利用  assert function 进行类型保证。 TS 编译器把位于该类函数被调用的位置之后，被传给该类函数的变量的类型视为<code>assert...is</code>指定的结果；或是把作为 a sserts condition 的表达式（使用了 J S 运算符<code>typeof</code>/<code>instanceof</code>/<code>in</code> ）中得到的类型信息作为随后的类型。该功能是个升级版的 type predicate</p>
</li>
<li>
<p><a title="https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html" href="https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html">type guard</a>，基于控制流中某些语句——例如搭配了 type narrowing（指“ 对 assert function 的调用、使用  JS 运算符<code>typeof</code>/<code>instanceof</code>/<code>in</code>或使用了  Discriminated Unions 中的 tag（见上文）以及 type predicate的情况） 的   <code>if...else</code>、<code>return</code>、<code>break</code>语句以及使用了<code>?:</code>运算符的表达式语句，自动推断出类型</p>
<ul>
<li>支持 <a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-and-non-undefined-type-guards" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-and-non-undefined-type-guards">non-null check</a></li>
<li>支持 [dotted names checking](<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#dotted-names-in-type-guards" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#dotted-names-in-type-guards">https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#dotted-names-in-type-guards</a></li>
</ul>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#destructured-variables-can-be-explicitly-marked-as-unused" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#destructured-variables-can-be-explicitly-marked-as-unused">在解构时显式标明无用变量</a>，避免 noUnused 报错</p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>TS 支持<a title="http://www.typescriptlang.org/docs/handbook/type-inference.html" href="http://www.typescriptlang.org/docs/handbook/type-inference.html"><strong>类型推断</strong></a>，在程序员没有手动指定类型的地方，编译器尽量根据上下文类型自动指定一个类型以用于静态类型检查（注意：必须熟悉 TS 的类型推断规则。由 TS 自动推断出的、并用于实施约束的类型很可能和程序员的预料不一样。别无他法时，可使用类型断言来覆盖类型推断的结果）</p>
<ul>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#improved-checking-for-destructuring-object-literal" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#improved-checking-for-destructuring-object-literal">对使用了解构赋值的变量的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-8.html#improved-checking-for-forin-statements" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-8.html#improved-checking-for-forin-statements">对 for…in 语句中的变量的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html#improved-inference-for-generics" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html#improved-inference-for-generics">对使用了泛型的类型的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#expression-operators" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#expression-operators">对使用了逻辑运算符的表达式的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#improved-type-inference-for-object-literals" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#improved-type-inference-for-object-literals">对使用对象字面量赋值的变量的推断规则</a>以及<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#generic-spread-expressions-in-object-literals" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#generic-spread-expressions-in-object-literals">使用对象展开操作符时的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#improved-behavior-for-calling-union-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#improved-behavior-for-calling-union-types">对 callable 类型的 union 的调用得到的值的类型的推断规则</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#higher-order-type-inference-from-generic-functions" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#higher-order-type-inference-from-generic-functions">对一个由使用了泛型类型的参数的<em>高阶函数</em>返回的函数的类型推断规则</a></p>
</li>
<li>
<p>有的情况下能<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#better-inference-for-literal-types" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#better-inference-for-literal-types">推断得到 literal types</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#higher-order-type-inference-from-generic-constructors" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#higher-order-type-inference-from-generic-constructors">Higher order type inference from generic constructors</a></p>
</li>
<li>
<p><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#improved-any-inference" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#improved-any-inference">尽可能不推断出 any</a></p>
</li>
</ul>
</li>
<li>
<p>TS 支持在<a title="http://www.typescriptlang.org/docs/handbook/generics.html#generic-types" href="http://www.typescriptlang.org/docs/handbook/generics.html#generic-types">函数、接口</a>以及<a title="http://www.typescriptlang.org/docs/handbook/generics.html#generic-classes" href="http://www.typescriptlang.org/docs/handbook/generics.html#generic-classes">类</a>（包括 <a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#generic-type-arguments-in-jsx-elements" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#generic-type-arguments-in-jsx-elements">JSX 表示的元素字面量</a>）上使用<strong>泛型类型</strong> （其中对函数允许用泛型类型<a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#generic-rest-parameters" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#generic-rest-parameters">约束剩余参数</a>） 。泛型类型是参数化的类型。被使用泛型类型所约束的东西在使用时可以为类型参数指定具体类型（即显式指定泛型参数），否则将依赖 TS 的类型推导确定类型参数的具体类型</p>
<ul>
<li>支持多个类型参数，<a title="http://www.typescriptlang.org/docs/handbook/generics.html#using-type-parameters-in-generic-constraints" href="http://www.typescriptlang.org/docs/handbook/generics.html#using-type-parameters-in-generic-constraints">它们之间可以构成约束关系</a></li>
<li>可<a title="http://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints" href="http://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints">令类型参数与类或接口构成约束（使用<code>extends</code>关键字）</a>。可以 <a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#type-parameters-that-extend-any-no-longer-act-as-any" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#type-parameters-that-extend-any-no-longer-act-as-any">extends any 类型</a></li>
<li><a title="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults" href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults">类型参数可以有默认值</a>。当使用泛型时没有在代码中直接指定类型参数，从实际值参数中也无法推测出时，这个默认类型就会起作用</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/generics.html#using-class-types-in-generics" href="http://www.typescriptlang.org/docs/handbook/generics.html#using-class-types-in-generics">如何对工厂函数应用泛型</a></li>
</ul>
</li>
</ul>
<h1 id="其它">其它</h1>
<ul>
<li>.d.ts 文件</li>
<li><a title="http://www.typescriptlang.org/docs/handbook/module-resolution.html" href="http://www.typescriptlang.org/docs/handbook/module-resolution.html">模块路径确定策略</a></li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[《Learn Prolog Now》读书笔记]]></title>
            <guid>01b1a419175046918467d80c30fcddba</guid>
            <pubDate>Sat, 23 Mar 2019 15:13:04 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="基本组成部分">基本组成部分</h1>
<p>使用一个 Prolog 的实现，读取/编译一个由 facts 和 rules 组成的 knowledge bases 后，向它发送 query 启动一次 称为 proof search 的 查询过程。 在这一查询过程中，Prolog 的实现将通过一种匹配（Uniﬁcation）机制，尽力将 query “匹配”（术语称 match 或 unify）到某个 fact 上 （这个 fact 可能是由 rules 推导得出的，而不是直接由程序员写在 knowledge base 里的）。匹配过程在 整个查询过程中一般会 发生多次。Prolog 编程指的就是编写 knowledge bases。</p>
<p>Knowledge base 完全由 clauses 组成， clauses 必由<code>.</code>结尾。每一条 clause 要么是 fact，要么是 rule。</p>
<ul>
<li>rule：每一条 rule 使用 <code>:-</code>隔开“后件”与“前件”，其左边的部分称为<em>head of rule</em>，右边部分称为<em>body</em>. 换句话说，<code>:-</code> 应当读作"if"，而<code>head :- body</code>读作“if the body of the rule is true, then the head of the rule is true”。其中，body 部分至少由一个 goal 组成，多个 goals 之间可用<code>,</code>隔开。<code>,</code>读作"and"。若想用不同的后件推导出同一个前件，即使用"or"连接多个前件，则应当在 body 处使用<code>;</code>连接多个前件。但可读性更高的方法是另起个新的 rule（仍然使用同样的后件，但前件不同）</li>
<li>fact：每一条 fact 不过是一条 degenerate rule，即一个无前件的 rule</li>
</ul>
<h1 id="基本语法">基本语法</h1>
<p>任何一个 fact、rule 和 query 均由一个 term 直接构成，且这个 term 要能组成一个 predicate（或称 procedure）。</p>
<p>有四种 term：</p>
<ul>
<li>atom：形式上以小写字母开头（如<code>a</code>、<code>cyl</code>、<code>a_b</code>），或是被单引号包裹（如<code>'d b'</code>），或是由一些特殊字符组成（如<code>+</code>、<code>:-</code>等。这种 atom 通常具有<strong>语义</strong>）</li>
<li>number：形如<code> 23</code>, <code>1001</code>, <code>0</code>, <code>-365</code></li>
<li>varaible：形式上由大写字母开头，唯一例外是<code>_</code>（匿名变量）。此类 term 有一种被  instantiate 的潜力——即被另一个 term 替代的潜力（作用见下文）。形式上相同的多个 varaible ，在每一个匹配过程中必被 instantiate 到同一个 term 上（匿名变量<code>_</code>是例外，多个<code>_</code>可以不被 instantiate 到同一个 term 上）</li>
<li>complex term：在形式上，由一个被称为 functor 的 atom 打头，后跟一个参数序列（由<code>()</code>包围，各参数使用<code>,</code>分割。其长度被称为  arity）。参数序列中的一个参数可以是任何一种 term。如果把 functor 定义为一个 operator，则该 functor 和作为其参数的 atom 可以以一种自定义的方式（前缀、中缀、后缀）排列（见9.4节，本文略）</li>
</ul>
<p>要组成一个 predicate，必须使用一个 atom 或一个 complex term。</p>
<h1 id="匹配unification一种形式上的满足">匹配（Uniﬁcation）：一种形式上的满足</h1>
<h2 id="匹配的定义">“匹配”的定义</h2>
<p>据前述，查询过程实质上是多次尝试让一个 query 匹配到某个 fact 的一个过程。一个 query 和一个 fact 均由一个 term 直接构成，因此 query 和 fact 的“匹配”问题归根结底是两个 term 的匹配问题。定义两个 term 匹配规则如下：</p>
<ol>
<li>If term1 and term2 are constants（atom 或 number 都是一种 constant）, then term1 and term2 match if and only if they are the same atom, or the same number.（值得一提的是<code>'aa'</code>和<code>aa</code>会被视为匹配的）</li>
<li>If term1 is a variable and term2 is any type of term, then term1 and term2 match, and term1 is <strong>instantiated</strong> to term2. Similarly, if term2 is a variable and term1 is any type of term, then term1 and term2 match, and term2 is instantiated to term1. (So if they are both variables, they’re both instantiated to a same new varibale, and we say that they share values.)</li>
<li>If term1 and term2 are complex terms, then they match if and only if:
<ol>
<li>They have the same functor and arity.</li>
<li>All their corresponding arguments match</li>
<li>and the variable instantiations are compatible. (I.e. it is not possible to instantiate variable X to mia, when matching one pair of arguments, and to then instantiate X to vincent, when matching another pair of arguments.)</li>
</ol>
</li>
<li>Two terms match if and only if it follows from the previous three clauses that they match.</li>
</ol>
<h2 id="proof-search构建搜索树">proof search——构建搜索树</h2>
<p>knowledge base</p>
<div><pre class="hljs"><code>f(a).

f(b).
g(a).
g(b).
h(b).
k(<span class="hljs-symbol">X</span>) :- f(<span class="hljs-symbol">X</span>),g(<span class="hljs-symbol">X</span>),h(<span class="hljs-symbol">X</span>).</code></pre></div>
<p>对查询<code>k(X). </code>有如下搜索树：</p>
<p><img src="https://image-1257651452.cos.ap-chengdu.myqcloud.com/20190323021602.png" /></p>
<blockquote>
<p>whenever it has a list of goals, Prolog tries to satisfy them one by one, working through the list in a left to right direction</p>
</blockquote>
<blockquote>
<p>Points in the search where there are several alternatives for matching a goal against the knowledge base are called <strong>choice points</strong>. Prolog keeps track of choice points and the choices that it has made there, so that if it makes a wrong choice, it can go back to the choice point and try something else. This is called <strong>backtracking</strong></p>
</blockquote>
<blockquote>
<p>The nodes of the tree say which are the goals that have to be satisﬁed at a certain point during the search and at the edges we keep track of the variable instantiations that are made when the current goal (i.e. the ﬁrst one in the list of goals) is match to a fact or the head of a rule in the knowledge base. Such trees are called <strong>search trees</strong> and they are a nice way of visualizing the steps that are taken in searching for a proof of some query. Leave nodes which still contain unsatisﬁed goals are point where Prolog failed, because it made a wrong decision somewhere along the path. Leave nodes with an empty goal list, correspond to a possible solution. The information on the edges along the path from the root node to that leave tell you what are the variable instantiations with which the query is satisﬁed</p>
</blockquote>
<h2 id="匹配算法unification-algorithms">匹配算法（unification algorithms）</h2>
<p>在深度优先搜索 search tree 的过程中，Prolog 使用一种 optimistic uniﬁcation algorithm 来为一个节点匹配。即不进行 occurs check（一种花费额外开销 peek inside the structure of the terms they are asked to unify，来在实施匹配之前判断能否匹配的机制） 。这可能导致匹配过程无法终止。</p>
<h2 id="附录unification-vs-pattern-match">附录：unification vs. pattern match</h2>
<table>
<thead>
<tr>
<th>pattern matching</th>
<th>unification</th>
</tr>
</thead>
<tbody>
<tr>
<td>Matches a syntactic pattern to a data object.</td>
<td>Matches a data object to a data object.</td>
</tr>
<tr>
<td>mutable variables or single-assignment variables</td>
<td>logical variables</td>
</tr>
<tr>
<td>one-sided: Only assigns to variables on the left.</td>
<td>two-sided: Assigns in both directions.</td>
</tr>
<tr>
<td>No constraints are created.</td>
<td>Can create equality constraints.</td>
</tr>
<tr>
<td>lexically restricted: Only assigns to variables that occur syntactically in the statement.</td>
<td>lexically unrestricted: Assigns to variables that do not occur syntactically in the statement.</td>
</tr>
<tr>
<td>sequential–Assignments must occur in the presented order.</td>
<td>non-sequential–Assignments can be done in any order.</td>
</tr>
<tr>
<td>non-idempotent–Assignments cannot generally be repeated.</td>
<td>idempotent–Assignments can be repeated without changing the effect.</td>
</tr>
<tr>
<td>A single pattern can match different terms with different structures.</td>
<td>A term can only match another term with the same structure.</td>
</tr>
</tbody>
</table>
<p>来源：<a title="http://www.unobtainabol.com/2016/04/unification-things-pattern-matching.html" href="http://www.unobtainabol.com/2016/04/unification-things-pattern-matching.html">http://www.unobtainabol.com/2016/04/unification-things-pattern-matching.html</a></p>
<h1 id="形式之外语义">形式之外：语义</h1>
<p>Prolog 预定义了一些具有“意义”的、   用于充当 functor 的 term。“有意义”的 term 指的是， Prolog 的实现<strong>并不（只）拿该 term 用于对某个 fact 的形式上的匹配，而是另有他用（也就是具有一定语义）</strong>。这一“他用”发生在一个 sandbox 中，程序员需要了解该处理将导致的效果（也就是其语义）来加以利用。</p>
<p>使用这些 term  充当 functor  的 complex term 不能充当 fact。因为 fact 不应该有语义。运行时只有 queries 和 rules 能携带并发挥语义。</p>
<h2 id="与"><code>=</code>与<code>==</code></h2>
<p><code>=/2</code>在语义上，判断两个参数是否匹配。这一判断过程若涉及变量，则会对变量进行 instantiate 操作。反义词<code>\=/2</code>；<code>==/2</code>类似，但若涉及变量，并不进行 instantiate 操作。反义词<code>\==/2</code></p>
<h2 id="与列表"><code>.</code>与列表</h2>
<p>Prolog 预定义了<code>[]</code>和<code>./2</code>这两个 term，它们合称 list constructors。<code>./2</code>的语义是“有序地排列两个参数组成一个 non-empty list，其中第二个参数必须是一个[]或一个 non-empty list”。</p>
<p>Prolog 提供了一种语法糖，使得程序员可以避免使用<code>./2</code>来构建 non-empty list。例如<code>.(.(a,[]),.(.(b,.(c,[])),[]))</code>可简写为<code>[[a],[b,c]]</code>。</p>
<p>对任意 non-empty list，可以在试图用一 non-empty list 匹配它时，使用<code>|</code>分割后者，使得后者能在一次匹配尝试中分别独立地匹配前者的两个部分（head 与 tail）。</p>
<p>non-empty list 是一种递归定义的结构（实际上是一棵二叉树），因此通常也使用具有递归形式的 rule 来处理它（该 rule 以 reduce 的方式进行处理是很常见且高效的）。利用<code>|</code>，可以通过具有递归形式的 rule 来递归匹配一个 non-empty lisy。</p>
<h2 id="is与算术"><code>is</code>与算术</h2>
<p><code>is/2</code>在语义上判断两个参数是否匹配。 这一判断过程要求第二个参数是由 <code>+/2</code>,<code>*/2</code>,<code>-/2</code>, <code>mod/2</code>充当 functor 的 complex term，该 complex term 将被转换为一个 number 用于匹配。</p>
<p>类似地，<code>&gt;/2</code>、<code>&gt;=/2</code>、<code>&lt;/2</code>、<code>&lt;=/2</code>、<code>=:=/2</code>、<code>=\=/2</code>在语义上判断第一个参数是否大/大等/小/小等/等/不等于第二个参数。两个参数均会被转换为一个 number 以进行匹配。</p>
<h2 id="examining-terms">Examining Terms</h2>
<h3 id="types-of-terms">Types of Terms</h3>
<p>Prolog provides a couple of built-in predicates that test whether a given term is of a certain type.</p>
<ul>
<li>atom/1 Tests whether the argument is an atom.</li>
<li>integer/1 Tests whether the argument is an integer, such as 4, 10, or-6.</li>
<li>float/1 Tests whether the argument is a ﬂoating point number, such as 1.3 or 5.0.</li>
<li>number/1 Tests whether the argument is a number, i.e. an integer or a ﬂoat</li>
<li>atomic/1 Tests whether the argument is a constant.</li>
<li>var/1 Tests whether the argument is uninstantiated.</li>
<li>nonvar/1 Tests whether the argument is instantiated.</li>
</ul>
<h3 id="the-structure-of-terms">The Structure of Terms</h3>
<ul>
<li>functor/3 will tell us what the functor and the arity of this term are</li>
<li>arg/3  tells us about arguments of complex terms. It takes a number N and a complex term T and returns the Nth argument of T in its third argument. It can be used to access the value of an argument</li>
<li>’=…’/2 takes a complex term and returns a list that contains the functor as ﬁrst element and then all the arguments</li>
</ul>
<h3 id="干涉-proof-search-过程">干涉 proof search 过程</h3>
<p>程序员可以使用以下 term 来干涉 proof search 过程。</p>
<ul>
<li>!/0：读作 cut。将其置于任何一个 goal 之后。这将阻止它左边的 goal 被 backtrace。这可能会影响程序的语义</li>
<li>fail/0：用于充当一个 goal，且对该 goal 的匹配将立刻失败。这将导致一个 backtrace。cut 常常与 fail 连用（作为两个连续的goal）组成一个 cut-fail combination，用于“辅助推导出某个后件”的例外前件（if…<strong>except for</strong>…then 中的 except for 部分）</li>
<li>+/1：Negation as failure. 相当于 cut-fail combination</li>
</ul>
<h1 id="在查询时操作-knowledge-base">在查询时操作 knowledge base</h1>
<p>见第11章。本文略</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[数据的流观点]]></title>
            <guid>aaa07dff5c0b427fbda68a3306b661c4</guid>
            <pubDate>Sun, 28 Oct 2018 15:08:08 GMT</pubDate>
            <content:encoded><![CDATA[<p>无论在一个软件系统的任何层面，当需要处理“<strong>历时过程</strong>中逐渐产生的（这段过程的终止时刻可能不存在或无法确定）数据”时，通常使用流的观点看待这些数据（尤其是当需要在这段过程发生期间处理这些数据时）。“数据历时产生”这一事实，可能是业务确实如此所致，也可能是程序员故意做的某种安排（例如通过分批将硬盘上的数据读入内存防止内存不足）。一种称为“响应式编程”的编程范式尤其适合这种情况。</p>
<p>综上，<strong>流是时间序列</strong>。时间序列与大部分程序员已经习惯使用的空间序列（有序的数据结构，例如列表、数组、字符串等）<strong>可以互相转化</strong>。</p>
<h1 id="构成流的事件">构成流的事件</h1>
<p>在该历时过程中产生的每一批数据被称为一个<strong>事件（event）</strong>。事件，作为描述了某个时刻的信息的数据，由系统中的<strong>生产者</strong>生成，其特点包括尺寸小、自描述（self-contained）、不可变（immutable）。事件的表现形式可能是人类可读文本、JSON、二进制数据等。人们通常会把相关的一批事件归入同一个 topic 或 stream 之中。</p>
<h1 id="流的使用">流的使用</h1>
<p>本节将讨论作为一种抽象的流的实际用法。</p>
<h2 id="流处理stream-processing">流处理（Stream Processing）</h2>
<p>一个流处理器（stream processor）通常有三种对流的可能处理：</p>
<ol>
<li>将流中的数据导入存储系统（例如数据库、缓存、搜索索引等）以备他用。利用数据库的 CDC 实现主从同步是典型应用。产生 CDC 流的数据库作为 leader，其它从库负责消费 CDC 流，该模式有效杜绝了非主从模式下使用 dual write（指应用程序显式地往多个数据库里写入数据） 的种种缺点，例如有很大的状态不同步的可能等</li>
<li>把流中的数据直接传给用户（通过电子邮件、通知系统等等）</li>
<li>对一个或多个输入流进行处理，输出一个流。这一处理过程被称作 <strong>operator</strong> 或 <strong>job</strong></li>
</ol>
<p>第3种情况的典型发生场景包括：</p>
<ul>
<li>Complex Event Processing（CEP）：一个 CEP 系统允许用户使用某种声明式语言（如 SQL、正则表达式等）从流中实时匹配数据，一旦匹配到，则产生一个 complex event。也就是说该系统利用一个或多个流生成一个流</li>
<li>对流进行搜索：和上一条类似，但允许用户使用更简单普适的查询方式（而非某种声明式语言）</li>
<li>流分析（Stream analytics）：如果对流的某些指标感兴趣，则可进行流分析，例如测量某类事件的发生比率/频率、计算流中数据在某窗口下的平均值、从流中发现某种趋势，等等。通常会使用概率算法（见《数据结构3：散列》）来提高计算效率</li>
<li>利用流维持物化视图（materialized views，见《3. SQL：DDL部分》）以供他用（通常是用作缓存）。实际上，<strong>一个应用程序可以看作一种通过用户输入流得以维持的物化视图</strong>（见下文 Event Sourcing 一节）</li>
<li>合并多个流（Stream joins）：将多个流合并成一个流后，可以更好地进行上述其它应用。有三种形式：
<ul>
<li>stream-stream join：例如为分析用户在使用搜索功能时的行为，我们将用户的搜索请求以及相应的返回视为一个个事件，它们构成一个流；用户在搜索页面的点击情视为另一类事件，构成另一个流。通过结合两个流，才能分析某一用户在某次搜索后没有点击哪些条目。进行 stream-stream join 时，流处理器需要维持一定状态，例如，维持根据 seesion ID 索引住的过去一个小时内所有的事件的哈希表，当两个流的新事件发生，都可以更新该哈希表，并可以发出一个事件表示用户的点击情况；每一小时后可以集中发送一个事件表示用户没有点击的情况（我们假设用户在搜索后的一小时没有点击意味着他已经放弃了搜索）</li>
<li>stream-table join(a.k.a stream enrichment)：有时我们想为一个流A中的事件根据我们已有的数据补充一些信息，输出一个由经过补充（enrich）的事件组成的新流。当流A出现新事件时就去数据库中进行相关信息的搜索很可能造成性能问题，一种解决方案是将可能用到的数据先加载到本地内存中或索引在本地硬盘上。这些本地数据可能会过期，这就需要由数据库的CAC机制产生的 table changelog 流来更新本地数据</li>
<li>table-table join(a.k.a materialized view maintenance)：利用多个流来维持一个物化视图（见上文）。这通常需要流处理器维护数据库表（常用作缓存），而这个表的内容通常与某句<code>SELECT...FROM...JOIN</code>查询的结果等价，因此得名 table-table join</li>
</ul>
</li>
</ul>
<p>三类 Stream joins 具有共同点，例如都需要流处理器跨事件地维持状态，而流处理器的状态受各流的各事件的时序影响。这就要求必须考虑实施 stream join 的时机。实际上，并不存在一个“正确的时机”，这意味着实施 stream join 的结果是不确定的（nondeterministic)：同样的流、同样的operator，多次应用的结果是不一致的。</p>
<h3 id="流处理时的时间问题">流处理时的时间问题</h3>
<p>在进行流处理，尤其是实时的流分析时，流处理器通常会设定一个时间窗口（time window），例如“最近五分钟”。但应注意到每个事件至少有两个时刻：由生产者生产出来的时刻（event time）和被流处理器处理的时刻（processing time），其中后者很可能有相当大的相对于前者的时延。除此之外可能有更复杂的情况，例如在手机APP上收集数据作为事件时，存在三种时刻：事件实际发生的时刻（由用户的手机的本地时钟确定，也就是 event time）、设备实发送该事件到服务器的时刻 （由用户的手机的本地时钟确定） 、服务器收到事件的时刻（也就是 processing time）。因此在处理流时设定时间窗口时必须弄清窗口到底是针对哪种时刻的，这决定了窗口的移动逻辑（当窗口决定移动时，称这个窗口 complete 了）。根据移动逻辑的不同，有以下常见的时间窗口类型：</p>
<ul>
<li>tumbling window：这种窗口有一个固定时长，且每个事件只会落到一个窗口里。例如一个“每五分钟移动一次的‘最近五分钟’窗口”</li>
<li>hopping window：有一个固定时长，不同窗口间会有重叠。例如一个“每一分钟移动一次的‘最近五分钟’窗口”</li>
<li>sliding window：有一个固定时长，窗口将在确保其中最旧的事件和最新的事件的时差不超过该固定时长的前提下尽可能多地收集事件</li>
<li>session window：不使用固定时长，而根据 seesion ID 来收集事件。当一个 session 在一段时间后没有任何消息，则关闭这个窗口。这种窗口尤其常见于网站分析</li>
</ul>
<p>当窗口发生移动后才姗姗来迟（可能因为网络传输延迟等）的、本应出现在上一个窗口内的事件被称为 straggler events。流处理器可以忽略这些事件（但应当做个记录，当出现太多 straggler events 时应当有所反应），也可以对此前的输出做一个 correction。</p>
<h3 id="应对流处理过程中的错误">应对流处理过程中的错误</h3>
<p>当流处理过程中出现错误时，不可能像一般的数据处理那样简单地抛弃所有处理结果然后重新计算一遍，而是启用基于一定数据粒度的恢复机制，出错时只须抛弃并重计算部分数据。属于一定粒度下的一批数据被称为 microbatching。粒度越小开销越大，但流处理过程的实时性越强。 另一种方案是使用 checkpoint，一旦出现错误，只抛弃上一个 checkpoint 之后的所有输出，并重新进行计算。</p>
<p>以上两种方案的缺点是都无法处理“流处理器的计算有副作用”的情况。为此须为流处理系统引入 atomic commit revisited 的概念，以确保一个流处理器在处理一个事件时，当且仅当没发生错误时才给出输出以及做有副作用的操作，且所有操作要么都成功进行，要么都不进行。实现该概念的方法包括 distributed transactions，或者依赖于操作的<strong>幂等性</strong>（idempotence）。任何操作都可以通过维护额外的元数据来确保幂等（例如为每个操作指定一个 ID，在实施每个操作前检查 ID 是否和已进行过的操作的 ID 重复了）。</p>
<p>最后，对于需要跨事件地维护状态的流处理器（例如使用任何形式的窗口的流处理器），在发生错误后必须能够恢复其状态。为此，流处理器可以通过某种方式把状态持久化。如果状态的维护是基于很短的窗口的，则通过重新计算也能恢复状态，就无需持久化了。</p>
<h2 id="event-sourcing">Event Sourcing</h2>
<p>流的一个重要用处是用作 event sourcing。一个程序在某个时刻的状态是从该程序运行起发生的所有事件（用户输入）造成的结果。因此，如果以不可变数据的形式记录（append-only log）下所有事件（或 changelog，是等价的），则可以令程序通过经历（replay） 这些事件进入指定状态（ 即 event sourcing ）。数据库在事实上只是事件日志（event log）的缓存（保存着最新的状态。而这些状态完全可以通过对changelog 进行 event sourcing 得到）。</p>
<p>基于显式的 event sourcing 思想设计的应用程序通常需要注意以下几点：</p>
<ul>
<li>使用 snapshots storing 机制的辅助，以避免重复进行 event sourcing 以来到某个状态</li>
<li>区分 commands 和 events：用户输入，在一开始应当被视为一个 command，只有当同步地验证这个 command 有效后，才能将它转换为一个或多个 event</li>
</ul>
<p>以不可变数据的形式保存 event log 的好处包括：</p>
<ul>
<li>应用程序从而可以回溯状态。这一特点可以用于实现某些业务，也可以用于 debug</li>
<li>event log 可以同时被不同主体以不同的方式使用。例如新旧系统交接、转换成不同的形式的数据存入相应的存储系统并确保一致性</li>
<li>简化并发控制。唯一需要考虑并发逻辑、对操作进行原子化的只有 event log 处</li>
</ul>
<p>不可变 event log 的麻烦之处体现在要真正删除它（例如储存空间不够/法律、政治强制力）的时候，因为由它派生的数据可能已出现在各处。</p>
<h1 id="流的传递消息系统及其实现">流的传递：消息系统及其实现</h1>
<p>为让系统中对事件感兴趣的主体（称为<strong>消费者</strong>）得到流，需要一种机制将某处产生的流传递给消费者。如果让消费者通过轮询（poll）等方式来检查是否有新事件出现 （pull 模型） ，将造成很多开销，因此通常在系统中加入一种通知机制（notification mechanism)，当事件出现时，消费者会通过某种方式（例如回调函数）<strong>被动地</strong>收到它感兴趣的事件（ push 模型）。</p>
<p>通知机制的实现是<strong>消息系统 （Messaging Systems） <strong>。消息系统基于</strong>发布/订阅模型（publish/subscribe model）</strong>：生产者（通常有多个）可以将事件推送至一个消息系统（该系统可能具有多个节点。每个节点可以分布在不同的线程/进程/物理机器中）中，随后该消息系统将事件发给所有感兴趣（即进行过<strong>订阅</strong>的）的消费者（通常有多个）。不同的消息系统的实现细节不尽相同，考察它们时关键在于把握以下两个问题：</p>
<ul>
<li>该消息系统如何处理“生产者生产事件的速度快于消费者处理事件的速度”这一情况？通常有三种选择：
<ul>
<li>作废未被消费者处理的事件</li>
<li>在一个队列中缓存（buffer）事件。此种情况应考察如果队列过长会发生什么</li>
<li>采用某种背压机制（backpressure），或称流控制机制（flow control），来阻止生产者的事件生产，直到消费者恢复了事件处理能力为止。这实际上是颠倒了一般情况下生产者和消费者的关系，后者对前者产生了主动的影响</li>
</ul>
</li>
<li>该消息系统的某个节点崩溃后会发生什么？生产者发送的消息是否会因此丢失？</li>
</ul>
<h2 id="实现生产者和消费者直接通信">实现：生产者和消费者直接通信</h2>
<p>消息系统最简单的实现就是让生产者直接和消费者通信。UDP多播就是一个典型例子。这种实现的缺点在于一旦消费者离线则生产者的消息都白白发送了，生产者崩溃后也不得不重新生产未发出的事件，造成了不必要的开销。另一方面，各生产者和各消费者间错综复杂的通信关系也加大了系统的复杂程度。</p>
<h2 id="实现消息队列message-queue">实现：消息队列（Message queue）</h2>
<p>消息队列又称消息中介（message broker），是常见的一种消息系统实现。通过维持一个消息服务器，并让生产者和消费者以客户端的身份使用它（各生产者将事件发给中介，中介将事件异步发给相应的消费者），来实现生产者和消费者的通信。</p>
<p>由于存在一个集中处理所有数据的中介，与数据持久性有关的处理可以统一由该中介进行。数据可以被储存在内存或硬盘中从而确保一定的持久性，因此面对处理速度赶不上消息生产速度的情况，消息不会被作废或是触发背压机制。</p>
<p>注意，由于是异步派发消息给消费者，一个消息从产生到被处理可能会经历相当可观的时延。</p>
<h3 id="消息队列与数据库的异同">消息队列与数据库的异同</h3>
<p>消息队列，同作为一个数据存放场所，和数据库有以下异同：</p>
<ul>
<li>消息队列通常是为在内存中快速进出的小数据集设计的，积累的数据太多（乃至需要它进行硬盘I/O）会严重影响它派发消息的性能</li>
<li>客户端可以以多种方式检索数据库和消息队列中的数据。不过消息队列一般并不支持非常特定的查询（arbitrary queries），但有能力在查询集更新后通知客户端（而数据库做不到这点，查询集更新后，客户端需要重新发起查询获得新数据）</li>
</ul>
<h3 id="对多消费者的消息派发">对多消费者的消息派发</h3>
<p>当多个消费者订阅了同一 topic 时，面对生产者发来的属于该 topic 的事件，消息队列一般有两种派发方案：</p>
<ul>
<li>根据某种负载均衡算法，将该消息派发给一个特定的消费者</li>
<li>将该消息发送给所有消费者（fan-out）</li>
</ul>
<p>应当结合具体的业务情况结合使用这两种方案。</p>
<h3 id="ack-和消息重传">Ack 和消息重传</h3>
<p>为确保发至消费者的事件确实被消费者处理了，消息队列会要求消费者客户端在处理完成后发送一个 ack，在此之后消息队列才会从队列中删掉这个消息。如果没收到 ack 则进行消息重传。为应对“事件已被消费者处理，只是 ack 因某种原因丢失了”的情况，通常会使用某种原子提交协议（atomic commit protocol，见 P360）。</p>
<p>注意，一旦触发重传，可能意味着各事件派发的时序将发生错乱。如果各事件是时序无关的，则这不是一个问题。</p>
<h2 id="实现log-based-消息系统">实现：log-based 消息系统</h2>
<p>一类常见的 log-based 消息系统（例如kafka）以 append-only log 的形式在硬盘上记录事件（而非先在内存中记录直到内存满了才写到硬盘）：生产者通过在日志文件末尾写入数据来传递信息，消费者通过顺序读取日志文件来得到信息，并在日志结尾处主动等待新内容的写入。其中，消费者应当维持一个状态记录自己消费到哪一个事件了（可方便别的消费者前来接手），消息系统则只需周期性地为各消费者做记录，从而减小开销。</p>
<p>日志通常可以分片、分机存放，以避免占用一个硬盘的太多空间（注意，占有空间过大时旧日志会被删除，包括未来得及被消费的部分。应用应当实现一个事件积压情况监控的机制）。生产者和消费者可以使用任何一片日志（当然，细节由消息系统为它们隐藏了）。</p>
<p>为实现负载均衡，消息系统可以令客户端（消费者或生产者）长期使用指定的分片，且每个客户端以单线程进行消费，注意这会导致需要准备同分片数量一样多的消费者，以及如果消费某个事件的时间过长，将阻塞之后的事件消费；要实现 fan-out 也容易，让所有消费者都来读取某个日志即可。</p>
<p>log-based 消息系统最适合事件吞吐量很高，但消费每个事件的开销较少的情况。</p>
<h1 id="现实中的流">现实中的流</h1>
<h2 id="cdc一种基于数据库的流的生产机制">CDC：一种基于数据库的流的生产机制</h2>
<p>数据库的 Change Data Capture（CDC）机制在近年来开始流行。实现该机制后，数据库会将每次数据改动的情况记录成 log 供外部读取，从而成为一种流的源。该类流可被用于导入其它数据存储系统（称为 derived data systems），且处在此类集群中时，生产 change data capture 流的数据库是集群的 leader。将该流完整记录下，可以恢复出完整的数据；或从某个时刻后部分地记录下，则可以基于该时刻的 snapshot 恢复出数据。为解决 log 过大的问题，可以引入 log compaction 机制，该机制通过完整分析 log，确认每个 key 的最新值，从而可以删掉该 key 的值的中间变化的过程。</p>
<p>在实现上， CDC 可借助数据库的触发器功能，但这样做的性能开销颇大；另一种常见做法是异步解析数据库的 changelog（用于数据库内部使用的数据文件）。</p>
<h2 id="nodejs-中的流stream">Node.js 中的流（Stream）</h2>
<p>本节介绍 Node.js 标准库中提供的 Stream 抽象。</p>
<p>Node.js 中的 Stream 除了表示上文所述的作为历时产生数据的源的抽象（<code>Readable</code>），也能<strong>作为数据的目的地</strong>的抽象（<code>Writable</code>、<code>Duplex</code>、<code>Transform</code>）。充当后者的 Stream 起到一个数据蓄水池的作用，例如用于控制程序输出至I/O的数据的量和频率，或是方便程序员之后从蓄水池中取出数据使用。</p>
<h3 id="readable-stream-的功能">Readable Stream 的功能</h3>
<p>Readable Stream  具有以下功能：</p>
<ul>
<li>
<p>支持两种模式为消费者产生数据：</p>
<ul>
<li>
<p>流动模式（flowing mode）：实际上就是使用 push 模型的流。进入该模式的 Readable Stream 对象通过<code>data</code>事件给出数据。令 Readable Stream 对象进入该模式的方法有：</p>
<ul>
<li>
<p>在对象的初始状态下订阅 其<code>data</code>事件</p>
</li>
<li>
<p>在对象的初始状态下调用<code>pipe</code>方法</p>
</li>
<li>
<p>在对象的暂停模式下调用<code>resume</code>方法</p>
</li>
</ul>
</li>
<li>
<p>暂停模式（paused mode）： 实际上就是使用 pull 模型的流。消费者通过订阅其<code>readable</code>事件来知道什么时候有数据可读，随后通过<code>read</code>方法读取之。进入该模式的方法有：</p>
<ul>
<li>在 Readable Stream 对象并没有 pipe 至一个目的地的前提下调用<code>pause</code>方法</li>
<li>移除其全部的 pipe 目的地</li>
</ul>
</li>
</ul>
</li>
<li>
<p>数据的产生必然是异步的，即必然发生在下一个 Tick 或之后</p>
</li>
<li>
<p>会维护一个内部缓冲区用于实现 pull 模型。待发放给消费者的数据都存放在缓冲区里。缓冲区里一有数据，就触发 <code>readable</code>事件 。缓冲区的大小可在构造 Stream 对象时设置</p>
</li>
<li>
<p>支持通过管道（<code>pipe</code>）把数据交给作为消费者的 Writable Stream。尽管此时将进入流动模式，但 Writable Stream 内部实际上是以 pull 模型的方式使用它的，因为   Writable Stream  需要根据自己的需要从 Readable Stream 处获得数据。这种 Writable Stream 作为消费者控制 Readable Stream  生产数据的节奏的机制称为背压 （back pressure） 机制</p>
</li>
</ul>
<h3 id="writable-stream-的功能">Writable Stream 的功能</h3>
<p>Writable Stream（含 <code>Writable</code>、<code>Duplex</code>、<code>Transform</code> ）具有以下功能：</p>
<ul>
<li>充当管道的目的地，并调节作为源的 Readable Stream 的生产。内部会维护一个缓冲区，根据缓冲区的情况按需拉取上游数据</li>
</ul>
<h2 id="基于流的响应式编程以-rxjs-为例">基于流的响应式编程：以 Rxjs 为例</h2>
<blockquote>
<p>流为模拟具有内部状态的对象提供了另一种方式，即——用流表示某对象内部状态的历时过程。从本质上说，流将时间显式地表示了出来，因此就松开了被模拟的世界里的时间与求值过程中事件发生的顺序之间的紧密联系。——《SICP》</p>
</blockquote>
<p>由于流自带时序信息，命令式编程所依赖的语句间的先后关系变得不再重要，甚至不再必要，这与声明式，尤其是函数式编程极为契合，因为 FP 中的纯函数们从来不关心时序（否则就不纯了）。</p>
<p>以 <a>Rxjs</a> 为例。Rxjs 中流被抽象为 Observable， 这是比 Node.js 中的 Stream 更抽象的事物。<a title="https://zhuanlan.zhihu.com/p/20241182" href="https://zhuanlan.zhihu.com/p/20241182">二者有何不同？Node 的 Stream 是和 Unix 哲学紧密契合的概念，非常好用，很简单，容易使用，这是它的优点；但它局限在 IO，通用性不如 Observable，而且提供的操作也仅仅限于 pipe 等最基础的操作，虽然有 event-stream 这样的第三方库加入了大量的实用操作（如map，join，split，merge等），但其功能丰富程度远不如Observable，为 composition 所做的努力也远不如 Observable。</a></p>
<p>Observable <strong>更适用于声明式编程。而 Node Stream 更应该被理解为一种对产生值、使用值的过程的过程式封装</strong>（证据是后者暴露了大量能够干涉数据生产过程的 API）。</p>
<p>通过在“流”这一抽象层次操作数据，程序员可以避免基于底层抽象维护程序状态，因为状态已经由历时的事件来表示了（甚至可以把程序视为“无状态”的。因为事件具有不可变的性质）。并且将流的生产逻辑（处理输入）和消费逻辑（改变内存中的数据/进行输出）解耦，也利于模块化。这种范式契合很多场景，例如 UI 编程。将用户在界面上的反复操作看作一个流，并基于它进行各种流处理、流传递操作，并得到一个输出流供其它内外组件消费（例如渲染界面）。</p>
]]></content:encoded>
        </item>
    </channel>
</rss>