https://geeknote.net/yuan
yuan
程序员
https://geeknote-storage.oss-cn-hongkong.aliyuncs.com/8lwsgofbdjz1rxims0ffi7zd179n?x-oss-process=image%2Fresize%2Cm_fill%2Cw_160%2Ch_160
2023-05-17T08:38:10Z
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/2274
2023-04-08T02:48:50Z
2023-05-17T08:38:10Z
TypeScript 笔记
<h1>
<a id="TypeScript" href="#TypeScript" class="anchor"></a>TypeScript</h1>
<h2>
<a id="%E9%A1%B9%E7%9B%AE%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84" href="#%E9%A1%B9%E7%9B%AE%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84" class="anchor"></a>项目文件结构</h2>
<h3>
<a id="tsconfig.json" href="#tsconfig.json" class="anchor"></a>tsconfig.json</h3>
<pre class="highlight"><code class="language-json"><span class="p">{</span><span class="w">
</span><span class="nl">"compilerOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"lib"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"es2015"</span><span class="p">],</span><span class="w">
</span><span class="nl">"module"</span><span class="p">:</span><span class="w"> </span><span class="s2">"commonjs"</span><span class="p">,</span><span class="w">
</span><span class="nl">"outDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dist"</span><span class="p">,</span><span class="w">
</span><span class="nl">"sourceMap"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"strict"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"es2015"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"include"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"src"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
<p>放于项目根目录下。</p>
<ul>
<li>include: TSC 在哪些文件夹中寻找 TypeScript 文件</li>
<li>lib: TSC 假定运行代码的环境中有哪些 API?(es2015, es2020, esnext 等等)编写在浏览器中运行的 TypeScript 需要加入 "dom"</li>
<li>module: TSC 把代码编译成哪个模块系统(commonjs, amd 等等)</li>
<li>outDir: 生成的 JavaScript 放置的目录</li>
<li>strict: TypeScript 严格模式(true, false)</li>
<li>target: TSC 把代码编译成哪个 JavaScript 版本(es2015, es2020, esnext 等等)</li>
</ul>
<h3>
<a id="tsling.json" href="#tsling.json" class="anchor"></a>tsling.json</h3>
<pre class="highlight"><code class="language-json"><span class="p">{</span><span class="w">
</span><span class="nl">"defaultSeverity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="nl">"extends"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"tslint:recommended"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"jsRules"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
</span><span class="nl">"rules"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"indent"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="s2">"spaces"</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">],</span><span class="w">
</span><span class="nl">"semicolon"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"trailing-comma"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"rulesDirectory"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
<p>文件生成命令:<code>./node_modules/.bin/tslint --init</code></p>
<p>可定制代码风格约定:tab 还是 space、用不用分号等。详见 <a href="https://palantir.github.io/tslint/rules">https://palantir.github.io/tslint/rules</a></p>
<h2>
<a id="%E7%B1%BB%E5%9E%8B" href="#%E7%B1%BB%E5%9E%8B" class="anchor"></a>类型</h2>
<p>TypeScript 使用类型注解来限制变量的类型,注解的方式是在其背后添加冒号和具体的类型,例如:<code>let n: number = 1</code> 限制 <code>n</code> 的类型为 <code>number</code>,<code>function fullname(p: Person): string</code> 限制参数 <code>p</code> 的类型为 <code>Person</code>,返回值类型为 <code>string</code>。</p>
<h3>
<a id="%E7%B1%BB%E5%9E%8B%E6%8E%A8%E6%96%AD" href="#%E7%B1%BB%E5%9E%8B%E6%8E%A8%E6%96%AD" class="anchor"></a>类型推断</h3>
<p>在没有显式声明变量类型时,TypeScript 会自动进行类型推断:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">m</span> <span class="o">=</span> <span class="mi">12</span>
<span class="kd">let</span> <span class="nx">s</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span>
</code></pre>
<p>如果用的是 VSCode 编辑器,将鼠标悬停在 <code>m</code> 和 <code>s</code> 上方可以看到它们的类型分别是 <code>number</code> 和 <code>string</code>。</p>
<p>但如果使用 <code>const</code> 而非 <code>let</code> 来声明变量,会看到 <code>m</code> 的类型变成了 <code>12</code>,<code>s</code> 的类型变成了 <code>"a string"</code>。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">const</span> <span class="nx">m</span> <span class="o">=</span> <span class="mi">12</span>
<span class="kd">const</span> <span class="nx">s</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span>
</code></pre>
<p>像这样的类型叫作字面量类型,完整的写法如下:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">const</span> <span class="nx">m</span><span class="p">:</span> <span class="mi">12</span> <span class="o">=</span> <span class="mi">12</span>
<span class="kd">const</span> <span class="nx">s</span><span class="p">:</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span>
</code></pre>
<h3>
<a id="any" href="#any" class="anchor"></a>any</h3>
<p><code>any</code> 类型的对象可以调用任意方法,访问任意属性,类型检查不起任何作用,就像一个常规的 JavaScript 对象。能不使用 <code>any</code> 的情况下,应该尽量避免使用。</p>
<p>在使用 <code>any</code> 类型时,必须显式注解,不能用类型推导。如果 TypeScript 推导出某个对象类型为 <code>any</code> 则会抛出运行时异常。</p>
<h3>
<a id="unknown" href="#unknown" class="anchor"></a>unknown</h3>
<p><code>unknown</code> 与 <code>any</code> 的主要区别是,你不能对 <code>unknown</code> 对象进行任何方法调用和属性访问,除非在上下文中显式地使用 <code>typeof</code> 或者 <code>instanceof</code> 对其进行过类型判断。但你可以直接对其使用 <code>==</code>, <code>===</code>, <code>||</code>, <code>&&</code>, <code>?</code>, <code>!</code>.</p>
<h3>
<a id="object" href="#object" class="anchor"></a>object</h3>
<p>在 TypeScript 中显式地将一个对象声明为 <code>object</code> 类型时,无法调用其任何方法,也无法访问其任何属性。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="nx">object</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">a</span><span class="p">)</span> <span class="c1">// 编译时出错</span>
</code></pre>
<p>但如果不显式声明类型:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">a</span><span class="p">)</span>
</code></pre>
<p>这样是可以正常编译和执行的。TypeScript 为自动推断 <code>o</code> 的类型为 <code>{a: number}</code>,这种类型被称作<strong>对象字面量类型</strong>。完整的写法如下:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">a</span><span class="p">)</span>
</code></pre>
<p>而将变量声明为 <code>object</code> 只表示该变量的类型是非原始类型,除此之外没有更具体的类型信息。</p>
<p>声明为 <code>Object</code>(首字母 O 大写)也类似。区别在于后者可以表示原始的封装类型,前者不行:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="nx">object</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span> <span class="c1">// 编译出错</span>
<span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="nb">Object</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span> <span class="c1">// 能正常执行</span>
</code></pre>
<p>但请尽量避免使用 <code>Object</code> 类型。</p>
<h3>
<a id="%E5%AF%B9%E8%B1%A1%E5%AD%97%E9%9D%A2%E9%87%8F%E7%B1%BB%E5%9E%8B" href="#%E5%AF%B9%E8%B1%A1%E5%AD%97%E9%9D%A2%E9%87%8F%E7%B1%BB%E5%9E%8B" class="anchor"></a>对象字面量类型</h3>
<p>前面的示例代码中使用了对象字面量类型:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">o</span><span class="p">.</span><span class="nx">a</span><span class="p">)</span>
</code></pre>
<p>这种类型告诉 TypeScript 编译器 <code>o</code> 这个对象的结构是含有 <code>a</code> 属性的一个对象,并且 <code>a</code> 属性的类型为 <code>number</code>。实际上,如果假设有一个类型 <code>C</code>,它的内部也有一个名为 <code>a</code> 的 <code>number</code> 属性,这时是可以将 <code>C</code> 的实例赋值给上面那个 <code>o</code> 的。</p>
<p>如果给对象字面量类型的变量赋值时,少了某个属性,或者多了某个属性,则会无法编译:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o1</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">b</span><span class="p">:</span> <span class="kr">string</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="kd">let</span> <span class="nx">o2</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">b</span><span class="p">:</span> <span class="mi">2</span><span class="p">}</span>
</code></pre>
<p>但多了属性的情况下,如果不以对象字面量的形式直接赋值,而是通过一个中间变量进行赋值,则可以正常运行:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">x</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">b</span><span class="p">:</span> <span class="mi">2</span><span class="p">}</span>
<span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">}</span> <span class="o">=</span> <span class="nx">x</span>
</code></pre>
<p>对象字面量类型中有一个特别的类型 <code>{}</code>,作用与 <code>Object</code> 类似,也尽量不要使用。</p>
<h3>
<a id="%E5%8F%AF%E9%80%89%E5%B1%9E%E6%80%A7" href="#%E5%8F%AF%E9%80%89%E5%B1%9E%E6%80%A7" class="anchor"></a>可选属性</h3>
<p>如果希望某个属性是可选的,可以使用<strong>可选属性</strong>:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">b</span><span class="p">?:</span> <span class="kr">string</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">b</span><span class="p">:</span> <span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span><span class="p">}</span>
</code></pre>
<p>像这样在 <code>b</code> 属性的后面加一个问号,就表示这个 <code>b</code> 属性是可选的。</p>
<h3>
<a id="%E7%B4%A2%E5%BC%95%E7%AD%BE%E5%90%8D" href="#%E7%B4%A2%E5%BC%95%E7%AD%BE%E5%90%8D" class="anchor"></a>索引签名</h3>
<p>如果希望拥有不确定个数的某个类型属性,可以使用<strong>索引签名</strong>:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="nl">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="p">[</span><span class="nx">key</span><span class="p">:</span> <span class="kr">number</span><span class="p">]:</span> <span class="nx">boolean</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">o</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="mi">1</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="mi">2</span><span class="p">:</span> <span class="kc">false</span><span class="p">}</span>
</code></pre>
<p><code>[key: number]: boolean</code> 表示该类型含有 0 个或多个属性名为 <code>number</code> 类型,属性值为 <code>boolean</code> 类型的属性。注意这里的 <code>key</code> 可以是任何合法的变量,不一定非得用 <code>key</code>,用 <code>index</code> 或者 <code>asdf</code> 都可以。并且它的类型必须是 <code>number</code> 或者 <code>string</code>,因为 JavaScript 对象通过 <code>[]</code> 取属性时只能传入数字或者字符串。</p>
<h3>
<a id="%E5%8F%AA%E8%AF%BB%E5%B1%9E%E6%80%A7" href="#%E5%8F%AA%E8%AF%BB%E5%B1%9E%E6%80%A7" class="anchor"></a>只读属性</h3>
<p>使用 <code>readonly</code> 可以限定某个属性为只读属性:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">o</span><span class="p">:</span> <span class="p">{</span><span class="k">readonly</span> <span class="nx">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="nx">o</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="mi">2</span> <span class="c1">// 无法修改</span>
</code></pre>
<h3>
<a id="%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D" href="#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D" class="anchor"></a>类型别名</h3>
<p>可以使用 <code>type</code> 给类型起一个别名,之后就可以拿这个别名来用作类型声明。使用类型别名的地方都可以替换成源类型:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Age</span> <span class="o">=</span> <span class="kr">number</span>
<span class="kd">let</span> <span class="nx">age</span><span class="p">:</span> <span class="nx">Age</span> <span class="o">=</span> <span class="mi">10</span>
<span class="kd">let</span> <span class="nx">n</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">20</span>
<span class="nx">age</span> <span class="o">=</span> <span class="kr">number</span>
</code></pre>
<p>由于 TypeScript 不会自行推断某个对象为类型别名,因此在需要使用的时候必须显式声明。</p>
<p>在同一作用域当中,类型别名无法重复赋值。与 <code>const</code> 和 <code>let</code> 类似,<code>type</code> 声明的类型别名具有块级作用域,块内部的声明将覆盖外部的声明。</p>
<h3>
<a id="%E4%BA%A4%E5%8F%89%E7%B1%BB%E5%9E%8B%E5%92%8C%E8%81%94%E5%90%88%E7%B1%BB%E5%9E%8B" href="#%E4%BA%A4%E5%8F%89%E7%B1%BB%E5%9E%8B%E5%92%8C%E8%81%94%E5%90%88%E7%B1%BB%E5%9E%8B" class="anchor"></a>交叉类型和联合类型</h3>
<p>将多个类型用 <code>&</code> 拼接起来,就是交叉类型(Intersection Types),声明为该类型的对象必须拥有所有类型的属性:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Cat</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">}</span>
<span class="kd">type</span> <span class="nx">Dog</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">barks</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="na">wags</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">}</span>
<span class="kd">type</span> <span class="nx">CatAndDog</span> <span class="o">=</span> <span class="nx">Cat</span> <span class="o">&</span> <span class="nx">Dog</span>
<span class="kd">let</span> <span class="nx">wat</span><span class="p">:</span> <span class="nx">CatAndDog</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">wat</span><span class="dl">'</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">barks</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">wags</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span>
<span class="kd">let</span> <span class="nx">wrongWat</span><span class="p">:</span> <span class="nx">CatAndDog</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">wat</span><span class="dl">'</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">wags</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span> <span class="c1">// 编译错误,缺少 barks 属性</span>
</code></pre>
<p>将多类类型用 <code>|</code> 连接起来,就是联合类型(Union Types),声明为该类型的对象必须是这些类型中的其中一种或多种:</p>
<pre class="highlight"><code class="language-typescript"><span class="c1">// ...</span>
<span class="kd">type</span> <span class="nx">CatOrDogOrBoth</span> <span class="o">=</span> <span class="nx">Cat</span> <span class="o">|</span> <span class="nx">Dog</span>
<span class="kd">let</span> <span class="nx">wat</span><span class="p">:</span> <span class="nx">CatOrDogOrBoth</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">wat</span><span class="dl">'</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span>
<span class="kd">let</span> <span class="nx">watWithoutName</span><span class="p">:</span> <span class="nx">CatOrDogOrBoth</span> <span class="o">=</span> <span class="p">{</span><span class="na">purrs</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span> <span class="c1">// 编译错误,缺少 name 属性</span>
<span class="kd">let</span> <span class="nx">watWithoutName</span><span class="p">:</span> <span class="nx">CatOrDogOrBoth</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">wat</span><span class="dl">'</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">wags</span><span class="p">:</span> <span class="kc">true</span><span class="p">}</span>
</code></pre>
<p>在一些情况下,你可能需要限制某个函数的返回值只能在某几个固定值当中取值,例如:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">rollDie</span><span class="p">():</span> <span class="mi">1</span> <span class="o">|</span> <span class="mi">2</span> <span class="o">|</span> <span class="mi">3</span> <span class="o">|</span> <span class="mi">4</span> <span class="o">|</span> <span class="mi">5</span> <span class="o">|</span> <span class="mi">6</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre>
<p>这个函数返回值只能是 1, 2, 3, 4, 5, 6 其中的一个。这种类型叫数字字面量类型。当然,还有字符串字面量类型,布尔字面量类型等。</p>
<h3>
<a id="%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E8%81%94%E5%90%88%E7%B1%BB%E5%9E%8B" href="#%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E8%81%94%E5%90%88%E7%B1%BB%E5%9E%8B" class="anchor"></a>数组中的联合类型</h3>
<p>TypeScript 可以对 <code>let a = [1, 2, 3]</code> 里的 <code>a</code> 推导出类型为 <code>number[]</code>。<code>let a = [1, 's']</code> 的类型则会被推导为 <code>(number|string)[]</code>。下面是稍微复杂点的情况:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">buildArray</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">a</span> <span class="o">=</span> <span class="p">[]</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">'</span><span class="s1">s</span><span class="dl">'</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">a</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">array</span> <span class="o">=</span> <span class="nx">buildArray</span><span class="p">()</span>
<span class="nx">array</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="c1">// 无法编译,(number|string)[] 类型中无法插入 boolean 值</span>
</code></pre>
<p>这种情况下,<code>buildArray()</code> 内部的 <code>a</code> 初始化时被推断为 <code>any[]</code> 类型,函数返回时类型确定,因此 <code>buildArray()</code> 的返回值被推断为 <code>(number|string)[]</code> 类型,所以外面的那个 <code>array</code> 无法再存入 <code>boolean</code> 值。</p>
<h3>
<a id="%E5%85%83%E7%BB%84%EF%BC%88Tuple%EF%BC%89" href="#%E5%85%83%E7%BB%84%EF%BC%88Tuple%EF%BC%89" class="anchor"></a>元组(Tuple)</h3>
<p>元组是数组的子类型,区别在于元组的长度是固定的,并且各索引位置上值的类型是已知的。由于元组字面量与数组字面量没有区别,因此在使用元组时必须显式声明其类型,否则会被自动推断成数组。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">tuple</span><span class="p">:</span> <span class="p">[</span><span class="kr">string</span><span class="p">,</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">boolean</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="kc">false</span><span class="p">]</span>
</code></pre>
<p>元组也支持可选元素和剩余元素:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">tuple</span><span class="p">:</span> <span class="p">[</span><span class="kr">string</span><span class="p">,</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">boolean</span><span class="p">?]</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span>
<span class="kd">let</span> <span class="nx">tuple2</span><span class="p">:</span> <span class="p">[</span><span class="kr">string</span><span class="p">,</span> <span class="kr">number</span><span class="p">,</span> <span class="p">...</span><span class="nx">boolean</span><span class="p">[]]</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">a string</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">false</span><span class="p">]</span>
</code></pre>
<h3>
<a id="null%2C+undefined%2C+void+%E5%92%8C+never" href="#null%2C+undefined%2C+void+%E5%92%8C+never" class="anchor"></a><code>null</code>, <code>undefined</code>, <code>void</code> 和 <code>never</code>
</h3>
<p><code>null</code> 表示空值,<code>undefined</code> 表示未定义,<code>void</code> 表示函数没有返回值,<code>never</code> 表示函数根本不返回(抛异常或者死循环)。</p>
<h3>
<a id="%E6%9E%9A%E4%B8%BE" href="#%E6%9E%9A%E4%B8%BE" class="anchor"></a>枚举</h3>
<pre class="highlight"><code class="language-typesctipt">enum Language {
Chinese,
English,
Russian
}
</code></pre>
<p>默认情况下枚举值为从 0 开始的数字,你也可以指定具体的值。枚举值可以是数字或字符串:</p>
<pre class="highlight"><code class="language-typesctipt">enum Language {
Chinese = 'Chinese',
English = 2
}
</code></pre>
<p>枚举值可以通过名字访问,也可以通过下标访问。不过通过下标访问容易出问题(访问不存在的位置),可以使用 <code>const</code> 来修饰枚举,这样 TypeScript 会强制你使用名字来访问枚举值。</p>
<p><code>const enum</code> 生成的 JavaScript 代码中不会含有对应定义枚举的代码,而是直接将枚举值内插到使用它的地方,例如直接将 <code>Language.English</code> 替换成 2。</p>
<h2>
<a id="%E5%87%BD%E6%95%B0" href="#%E5%87%BD%E6%95%B0" class="anchor"></a>函数</h2>
<p>一个完整的函数定义是这样的:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">add</span><span class="p">(</span><span class="nx">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">b</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
<span class="p">}</span>
</code></pre>
<p>多数情况下,参数的类型无法推断(除非是默认参数),因此需要声明类型。而返回值类型多数时候是可以推断出来的,因此可以省去类型声明。</p>
<h3>
<a id="%E5%8F%AF%E9%80%89%E5%8F%82%E6%95%B0" href="#%E5%8F%AF%E9%80%89%E5%8F%82%E6%95%B0" class="anchor"></a>可选参数</h3>
<p>函数的参数同样可以用 <code>?</code> 标记为可选参数。如果参数中同时包含可选参数和必要的参数,可选参数必须放在必选参数的后面。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">log</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">userId</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">time</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toLocaleTimeString</span><span class="p">()</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">time</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">userId</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">Not signed in</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<h3>
<a id="%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0" href="#%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0" class="anchor"></a>默认参数</h3>
<p>默认参数与 JavaScript 一样,不赘述。</p>
<h3>
<a id="%E5%89%A9%E4%BD%99%E5%8F%82%E6%95%B0" href="#%E5%89%A9%E4%BD%99%E5%8F%82%E6%95%B0" class="anchor"></a>剩余参数</h3>
<p>参数名前面加上 <code>...</code> 会使该参数变成一个数组,其中存放着参数列表中未定义的所有剩余参数。一个函数中只能有一个剩余参数,而且必须是参数列表中的最后一个参数。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">log</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="p">...</span><span class="nx">additionalInfo</span><span class="p">:</span> <span class="kr">string</span><span class="p">[])</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">time</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toLocaleTimeString</span><span class="p">()</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">time</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">additionalInfo</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">, </span><span class="dl">'</span><span class="p">))</span>
<span class="p">}</span>
<span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Hello</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">and</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">goodbye</span><span class="dl">'</span><span class="p">)</span>
</code></pre>
<h3>
<a id="this+%E5%8F%82%E6%95%B0" href="#this+%E5%8F%82%E6%95%B0" class="anchor"></a>this 参数</h3>
<p>和 JavaScript 一样,TypeScript 的函数在调用时,内部的 <code>this</code> 有可能指向不同的对象。如果你想限制函数内部的 <code>this</code> 指向对象的类型,可以使用 this 参数:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">fancyDate</span><span class="p">(</span><span class="k">this</span><span class="p">:</span> <span class="nb">Date</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">getFullYear</span><span class="p">()}</span><span class="s2">-</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">getMonth</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">getDate</span><span class="p">()}</span><span class="s2">`</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">fancyDate</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">)())</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">fancyDate</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="dl">"</span><span class="s2">2022-02-02</span><span class="dl">"</span><span class="p">)())</span> <span class="c1">// 无法编译</span>
</code></pre>
<p>this 参数必须放在参数列表的最前面,并且实际上它并不占用参数列表的位置,是个“假参数”。</p>
<h3>
<a id="%E7%94%9F%E6%88%90%E5%99%A8%E5%87%BD%E6%95%B0" href="#%E7%94%9F%E6%88%90%E5%99%A8%E5%87%BD%E6%95%B0" class="anchor"></a>生成器函数</h3>
<p>函数声明时,在 <code>function</code> 后面加一个星号,该函数就成为了一个生成器。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span><span class="o">*</span> <span class="nx">createFibonacciGenerator</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">a</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kd">let</span> <span class="nx">b</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">while</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 无限循环</span>
<span class="k">yield</span> <span class="nx">a</span><span class="p">;</span> <span class="c1">// 使用 yield 产出值,generator.next() 的值从这里来。注意分号。</span>
<span class="p">[</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">b</span><span class="p">,</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">generator</span> <span class="o">=</span> <span class="nx">createFibonacciGenerator</span><span class="p">()</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 0, done: false }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 1, done: false }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 1, done: false }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 2, done: false }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 3, done: false }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">generator</span><span class="p">.</span><span class="nx">next</span><span class="p">())</span> <span class="c1">//{ value: 5, done: false }</span>
</code></pre>
<p>生成器函数是惰性的,只在调用 <code>.next()</code> 时执行一次,然后就停止执行,直到下一次调用 <code>.next()</code>,因此上面的死循环并不会让程序卡死。这里执行的结果中,<code>done</code> 一直是 <code>false</code>。如果生成器内部不是无限循环,我们就有机会看到 <code>{done: true}</code> 的结果。</p>
<p>调用 <code>createFibonacciGenerator()</code> 得到的是一个 <code>IterableIterator<number></code> 迭代器。</p>
<h3>
<a id="%E8%BF%AD%E4%BB%A3%E5%99%A8" href="#%E8%BF%AD%E4%BB%A3%E5%99%A8" class="anchor"></a>迭代器</h3>
<p>迭代器是一个实现了 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol">Iterator protocol</a> 的任意对象。该协议要求对象里含有一个 <code>next()</code> 方法,该方法返回一个带有 <code>value</code> 和 <code>done</code> 属性的对象,正如上面的 <code>generator</code>。生成器本身同时也是一个迭代器。而迭代器可以有很多种。</p>
<h3>
<a id="%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1" href="#%E5%8F%AF%E8%BF%AD%E4%BB%A3%E5%AF%B9%E8%B1%A1" class="anchor"></a>可迭代对象</h3>
<p>可迭代对象拥有 <code>Symbol.iterator</code> 属性,并且该属性是一个函数,其中也使用 <code>yield</code> 产出值。对可迭代对象,可以使用 <code>for...of</code> 来迭代。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">numbers</span> <span class="o">=</span> <span class="p">{</span>
<span class="o">*</span><span class="p">[</span><span class="nb">Symbol</span><span class="p">.</span><span class="nx">iterator</span><span class="p">]()</span> <span class="p">{</span>
<span class="k">for</span><span class="p">(</span><span class="kd">let</span> <span class="nx">n</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">n</span> <span class="o"><=</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">n</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="k">yield</span> <span class="nx">n</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">for</span><span class="p">(</span><span class="kd">let</span> <span class="nx">n</span> <span class="k">of</span> <span class="nx">numbers</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">n</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>JavaScript 内置的常用集合类型(Array, Map, Set, String)都是可迭代对象。可迭代对象除了可以用 <code>for...of</code> 来操作,还可以像一个数组一样用 <code>...</code> 展开,也可以像数组一样被解构。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="p">[</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">]</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">a string</span><span class="dl">"</span>
</code></pre>
<h3>
<a id="%E5%87%BD%E6%95%B0%E7%AD%BE%E5%90%8D" href="#%E5%87%BD%E6%95%B0%E7%AD%BE%E5%90%8D" class="anchor"></a>函数签名</h3>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">sum</span><span class="p">(</span><span class="nx">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">b</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
<span class="p">}</span>
</code></pre>
<p>以上函数的签名是 <code>(x: number, y: number) => number</code>。可以换一种写法:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Sum</span> <span class="o">=</span> <span class="p">(</span><span class="nx">x</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">y</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="o">=></span> <span class="kr">number</span>
<span class="kd">let</span> <span class="nx">sum</span><span class="p">:</span> <span class="nx">Sum</span> <span class="o">=</span> <span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
<span class="p">}</span>
</code></pre>
<p>这种写法相当于定义了一个函数接口 <code>Sum</code>,实现该接口的函数(例如这里的 <code>sum()</code>)都必须有两个 <code>number</code> 类型的参数,并且返回一个 <code>number</code> 类型的值。</p>
<p>因为 <code>Sum</code> 已经定义了参数的类型,<code>sum()</code> 又被指定为了 <code>Sum</code> 类型。因此 <code>sum()</code> 在实现时,参数不再需要指定类型,类型信息可以直接从 <code>Sum</code> 的定义中推断得出。</p>
<p>需要注意的是,带有默认值参数的函数,和带有可选参数的函数的签名是一样的:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Log</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">userId</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=></span> <span class="k">void</span>
<span class="kd">let</span> <span class="nx">log</span><span class="p">:</span> <span class="nx">Log</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">userId</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Not signed in</span><span class="dl">'</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">time</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">()</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">time</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">userId</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>实际上,函数签名的完整写法如下:</p>
<pre class="highlight"><code class="language-typescript"><span class="c1">// type Log = (message: string, userId?: string) => void</span>
<span class="kd">type</span> <span class="nx">Log</span> <span class="o">=</span> <span class="p">{</span>
<span class="p">(</span><span class="na">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">userId</span><span class="p">?:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span> <span class="c1">// 这里需要把箭头改为冒号</span>
<span class="p">}</span>
</code></pre>
<p>实际上这里的 <code>type Log =</code> 可以替换成 <code>interface Log</code>,所以前文一直在使用“接口”、“实现”这样的概念。</p>
<h3>
<a id="%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD" href="#%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD" class="anchor"></a>函数重载</h3>
<p>下面是一个预订机票的函数:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Reserve</span> <span class="o">=</span> <span class="p">(</span><span class="k">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">to</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">destination</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=></span> <span class="k">void</span>
<span class="kd">let</span> <span class="nx">reserve</span><span class="p">:</span> <span class="nx">Reserve</span> <span class="o">=</span> <span class="p">(</span><span class="k">from</span><span class="p">,</span> <span class="nx">to</span><span class="p">,</span> <span class="nx">destination</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre>
<p>假如要加入单程旅行的支持,则需要提供一个省去 <code>to</code> 参数的函数:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Reserve</span> <span class="o">=</span> <span class="p">{</span>
<span class="p">(</span><span class="na">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="na">to</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="na">destination</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span>
<span class="p">(</span><span class="na">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="na">destination</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">reserve</span><span class="p">:</span> <span class="nx">Reserve</span> <span class="o">=</span> <span class="p">(</span><span class="k">from</span><span class="p">,</span> <span class="nx">to</span><span class="p">,</span> <span class="nx">destination</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="c1">// 编译出错</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre>
<p>这里编译会出错,因为声明为 <code>Reserve</code> 的 <code>reserve</code> 并没有实现所有的函数签名,此时,需要将代码修改为:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">reserve</span><span class="p">:</span> <span class="nx">Reserve</span> <span class="o">=</span> <span class="p">(</span><span class="k">from</span><span class="p">,</span> <span class="nx">toOrDestination</span><span class="p">:</span> <span class="nb">Date</span> <span class="o">|</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">destination</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// 代码中需要根据参数的不同情况来执行不同的逻辑</span>
<span class="p">}</span>
</code></pre>
<p>以上的写法是通过接口来定义重载,还有另一种方式,是用函数声明来定义重载:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">function</span> <span class="nx">reserve</span><span class="p">(</span><span class="k">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">to</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">destination</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span>
<span class="kd">function</span> <span class="nx">reserve</span><span class="p">(</span><span class="k">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">destination</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span>
<span class="kd">function</span> <span class="nx">reserve</span><span class="p">(</span><span class="k">from</span><span class="p">:</span> <span class="nb">Date</span><span class="p">,</span> <span class="nx">toOrDestination</span><span class="p">:</span> <span class="nb">Date</span> <span class="o">|</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">destination</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre>
<p>这只是两种不同的写法。</p>
<h2>
<a id="%E6%B3%9B%E5%9E%8B" href="#%E6%B3%9B%E5%9E%8B" class="anchor"></a>泛型</h2>
<p>// 略</p>
<h2>
<a id="%E7%B1%BB" href="#%E7%B1%BB" class="anchor"></a>类</h2>
<h3>
<a id="%E8%AE%BF%E9%97%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6" href="#%E8%AE%BF%E9%97%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6" class="anchor"></a>访问修饰符</h3>
<p>TypeScript 中,类的成员默认都是 public 的。</p>
<p>TypeScript 使用的是结构性类型系统。 比较两种不同的类型时,TypeScript 并不在乎它们具体的类型是什么,只要所有成员的类型都是兼容的,就认为它们的类型是兼容的。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nx">Zebra</span> <span class="p">{</span>
<span class="nx">trot</span><span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">Poodle</span> <span class="p">{</span>
<span class="nx">trot</span><span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">ambleAround</span><span class="p">(</span><span class="nx">animal</span><span class="p">:</span> <span class="nx">Zebra</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">animal</span><span class="p">.</span><span class="nx">trot</span><span class="p">()</span>
<span class="p">}</span>
</code></pre>
<p>以上两个类的实例都可以传入 <code>ambleAround()</code> 函数,在 TypeScript 中不会有任何问题。</p>
<p>但是在类中含有 private 或 protected 成员时,结果就不一样了。当两个类只是拥有同样的 private/protected 成员时,这两个类型不一定是兼容的,只有当它们的 private/protected 成员来自同一处声明时,这两个类型才能算兼容。下面的代码展示了这一点:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nx">Animal</span> <span class="p">{</span>
<span class="k">private</span> <span class="nx">name</span><span class="p">:</span> <span class="kr">string</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">theName</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">theName</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">Rhino</span> <span class="kd">extends</span> <span class="nx">Animal</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span> <span class="k">super</span><span class="p">(</span><span class="dl">"</span><span class="s2">Rhino</span><span class="dl">"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">Employee</span> <span class="p">{</span>
<span class="k">private</span> <span class="nx">name</span><span class="p">:</span> <span class="kr">string</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">theName</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">theName</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">animal</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Animal</span><span class="p">(</span><span class="dl">"</span><span class="s2">Goat</span><span class="dl">"</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">rhino</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Rhino</span><span class="p">()</span>
<span class="kd">let</span> <span class="nx">employee</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Employee</span><span class="p">(</span><span class="dl">"</span><span class="s2">Bob</span><span class="dl">"</span><span class="p">)</span>
<span class="kd">function</span> <span class="nx">printName</span><span class="p">(</span><span class="nx">o</span><span class="p">:</span> <span class="nx">Animal</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="nx">printName</span><span class="p">(</span><span class="nx">employee</span><span class="p">)</span> <span class="c1">// 错误: Animal 与 Employee 不兼容.</span>
</code></pre>
<p>把代码中的 <code>private</code> 修饰符去掉,这段代码才不会报错。</p>
<p>TypeScript 的 <code>protected</code> 表现与 Java 相同:在子类中可以访问。</p>
<h3>
<a id="%E5%8F%82%E6%95%B0%E5%B1%9E%E6%80%A7" href="#%E5%8F%82%E6%95%B0%E5%B1%9E%E6%80%A7" class="anchor"></a>参数属性</h3>
<p>TypeScript 可以在构造函数的参数中直接声明成员变量,这叫作“参数属性”。下面两段代码是等价的:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nx">Octopus</span> <span class="p">{</span>
<span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="kd">constructor</span> <span class="p">(</span><span class="nx">theName</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">theName</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">Octopus</span> <span class="p">{</span>
<span class="kd">constructor</span> <span class="p">(</span><span class="k">public</span> <span class="nx">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>参数属性通过给构造函数参数前面添加一个访问限定符来声明,可以是 <code>public</code>, <code>protected</code>, <code>private</code>, <code>readonly</code>。</p>
<h3>
<a id="super" href="#super" class="anchor"></a>super</h3>
<p>在子类中调用父类的构造函数,用 <code>super()</code>。在子类中调用父类的其它方法,用 <code>super.method()</code>。<code>super</code> 不能访问父类的属性。</p>
<h3>
<a id="%E8%BF%94%E5%9B%9E+this" href="#%E8%BF%94%E5%9B%9E+this" class="anchor"></a>返回 <code>this</code>
</h3>
<p>类似下面这种情况,子类和父类中两个方法逻辑一样,只是返回的类型不同,可以用返回 <code>this</code> 的方式消除重复:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nb">Set</span> <span class="p">{</span>
<span class="nx">add</span><span class="p">(</span><span class="nx">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nb">Set</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="k">return</span> <span class="k">this</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">MutableSet</span> <span class="kd">extends</span> <span class="nb">Set</span> <span class="p">{</span>
<span class="nx">add</span><span class="p">(</span><span class="nx">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nx">MutableSet</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="k">return</span> <span class="k">this</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>让 <code>add()</code> 返回 <code>this</code> 之后:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nb">Set</span> <span class="p">{</span>
<span class="nx">add</span><span class="p">(</span><span class="nx">value</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="k">this</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="k">return</span> <span class="k">this</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">MutableSet</span> <span class="kd">extends</span> <span class="nb">Set</span> <span class="p">{}</span>
</code></pre>
<h3>
<a id="extends+%E5%85%B3%E9%94%AE%E5%AD%97" href="#extends+%E5%85%B3%E9%94%AE%E5%AD%97" class="anchor"></a>extends 关键字</h3>
<p>常见的用法是放在用 <code>class</code>, <code>interface</code> 的后面,表示继承。</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">Everything</span> <span class="p">{}</span>
<span class="kr">interface</span> <span class="nx">Something</span> <span class="kd">extends</span> <span class="nx">Everything</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="nx">Super</span> <span class="k">implements</span> <span class="nx">Everything</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="nx">Sub</span> <span class="kd">extends</span> <span class="nx">Super</span> <span class="p">{}</span>
</code></pre>
<p>还可以出现在泛型参数里面,表示泛型类型必须是某个指定类型本身或其子类。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nx">StringContainer</span><span class="o"><</span><span class="nx">T</span> <span class="kd">extends</span> <span class="kr">string</span><span class="o">></span> <span class="p">{}</span>
</code></pre>
<p>还有一种用法,是用于声明一个类型时,放在两个类之间,判断前者是否是后者的子类。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">x</span> <span class="o">=</span> <span class="nx">Sub</span> <span class="kd">extends</span> <span class="nx">Super</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span>
</code></pre>
<p>在该例中,如果 <code>Sub</code> 是 <code>Super</code> 的子类,那么 <code>x</code> 为字面量类型 <code>0</code>,否则为字面量类型 <code>1</code>。</p>
<p>但当它出现在泛型中时,又比较特殊:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">G</span><span class="o"><</span><span class="nx">T</span><span class="o">></span> <span class="o">=</span> <span class="nx">T</span> <span class="kd">extends</span> <span class="dl">'</span><span class="s1">s</span><span class="dl">'</span> <span class="p">?</span> <span class="kr">string</span> <span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
<span class="kd">type</span> <span class="nx">X</span> <span class="o">=</span> <span class="nx">G</span><span class="o"><</span><span class="dl">'</span><span class="s1">s</span><span class="dl">'</span> <span class="o">|</span> <span class="mi">0</span><span class="o">></span>
</code></pre>
<p>此时,应该先把 <code>G<'s' | 0></code> 中的 <code>'s'</code> 代入 <code>type G<T></code> 得到 <code>string</code>,再将 <code>0</code> 代入 <code>type G<T></code> 得到 <code>number</code>,结果 <code>X</code> 的类型是 <code>string | number</code>。</p>
<h3>
<a id="%E5%85%B6%E5%AE%83" href="#%E5%85%B6%E5%AE%83" class="anchor"></a>其它</h3>
<p>TypeScript 类支持静态属性:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">class</span> <span class="nx">Grid</span> <span class="p">{</span>
<span class="k">static</span> <span class="nx">origin</span> <span class="o">=</span> <span class="p">{</span><span class="na">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>也支持抽象类:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">abstract</span> <span class="kd">class</span> <span class="nx">Animal</span> <span class="p">{</span>
<span class="kd">abstract</span> <span class="nx">makeSound</span><span class="p">():</span> <span class="k">void</span>
<span class="nx">move</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">roaming the earch...</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<h2>
<a id="%E6%8E%A5%E5%8F%A3" href="#%E6%8E%A5%E5%8F%A3" class="anchor"></a>接口</h2>
<p>实际上前面已经一直在用接口了:</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">type</span> <span class="nx">Cat</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">purrs</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">}</span>
<span class="c1">// 相当于</span>
<span class="kr">interface</span> <span class="nx">Cat</span> <span class="p">{</span>
<span class="nl">name</span><span class="p">:</span> <span class="kr">string</span>
<span class="nx">purrs</span><span class="p">:</span> <span class="nx">boolean</span>
<span class="p">}</span>
</code></pre>
<p>两者用起几乎一样,但有些细微差别。比如在同一作用域中用 <code>interface</code> 声明的多个接口会自动合并。但用 <code>type</code> 声明多个同名接口(类型)会出错:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">I</span> <span class="p">{</span>
<span class="nl">prop1</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">}</span>
<span class="kr">interface</span> <span class="nx">I</span> <span class="p">{</span>
<span class="nl">prop2</span><span class="p">:</span> <span class="nx">boolean</span>
<span class="p">}</span>
<span class="c1">// 相当于:</span>
<span class="kr">interface</span> <span class="nx">I</span> <span class="p">{</span>
<span class="nl">prop1</span><span class="p">:</span> <span class="kr">string</span>
<span class="nx">prop2</span><span class="p">:</span> <span class="nx">boolean</span>
<span class="p">}</span>
</code></pre>
<p>接口的属性可以是可选的:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">SquareConfig</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">?:</span> <span class="kr">string</span>
<span class="nx">width</span><span class="p">?:</span> <span class="kr">number</span>
<span class="p">}</span>
</code></pre>
<p>也可以是只读的:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">Point</span> <span class="p">{</span>
<span class="k">readonly</span> <span class="nx">x</span><span class="p">:</span> <span class="kr">number</span>
<span class="k">readonly</span> <span class="nx">y</span><span class="p">:</span> <span class="kr">number</span>
<span class="p">}</span>
</code></pre>
<p>TypeScript 提供了一个 <code>ReadonlyArray<T></code> 类型,是 <code>Array<T></code> 去掉了写操作的只读版本,可以确保数组创建之后不能被修改。</p>
<pre class="highlight"><code class="language-typescript"><span class="kd">let</span> <span class="nx">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
<span class="kd">const</span> <span class="nx">ro</span><span class="p">:</span> <span class="nx">ReadonlyArray</span><span class="o"><</span><span class="kr">number</span><span class="o">></span> <span class="o">=</span> <span class="nx">a</span>
<span class="nx">ro</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">12</span> <span class="c1">// error!</span>
<span class="nx">ro</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span> <span class="c1">// error!</span>
<span class="nx">ro</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">100</span> <span class="c1">// error!</span>
<span class="nx">a</span> <span class="o">=</span> <span class="nx">ro</span> <span class="c1">// error! 但可以强制转换: `a = ro as number[]`</span>
</code></pre>
<p>接口也可以定义函数类型:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">SearchFunc</span> <span class="p">{</span>
<span class="p">(</span><span class="nx">source</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">subString</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">boolean</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">mySearch</span><span class="p">:</span> <span class="nx">SearchFunc</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">a</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">b</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">a</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">b</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">result</span> <span class="o">></span> <span class="o">-</span><span class="mi">1</span>
<span class="p">}</span>
</code></pre>
<p>注:上例中,实现了 <code>SearchFunc</code> 接口的 <code>mySearch()</code> 函数,参数中的 <code>string</code> 类型声明其实可以省去。</p>
<p>接口还能用来定义“可索引类型”:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">StringArray</span> <span class="p">{</span>
<span class="p">[</span><span class="nx">i</span><span class="p">:</span> <span class="kr">number</span><span class="p">]:</span> <span class="kr">string</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">myArray</span><span class="p">:</span> <span class="nx">StringArray</span> <span class="o">=</span> <span class="p">[</span><span class="dl">"</span><span class="s2">Bob</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Fred</span><span class="dl">"</span><span class="p">]</span>
<span class="kd">const</span> <span class="nx">myStr</span><span class="p">:</span> <span class="kr">string</span> <span class="o">=</span> <span class="nx">myArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</code></pre>
<p>这个 <code>StringArray</code> 接口要求其实现必须可以通过 <code>[n]</code> 的方式读取数据,其中 <code>n</code> 为 <code>number</code> 类型,并且返回一个 <code>string</code>。</p>
<p>TypeScript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引。但是数字索引的返回值必须与字符串索引返回值的类型相同,或者是其子类型。 这是因为当使用 <code>number</code> 来索引时,JavaScript 会将它转换成 <code>string</code> 然后再去索引对象。</p>
<p>另外,因为 JavaScript 中,所有对象的属性都可以通过 <code>[key]</code> 的方式获得,因此,在接口中定义了字符串索引签名后,该接口的所有其它属性返回值都必须与之相同:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">NumberDictionary</span> <span class="p">{</span>
<span class="p">[</span><span class="nx">index</span><span class="p">:</span> <span class="kr">string</span><span class="p">]:</span> <span class="kr">number</span>
<span class="nx">length</span><span class="p">:</span> <span class="kr">number</span> <span class="c1">// 可以,length是number类型</span>
<span class="nx">name</span><span class="p">:</span> <span class="kr">string</span> <span class="c1">// 错误,`name`的类型与索引类型返回值的类型不匹配</span>
<span class="p">}</span>
</code></pre>
<p>(没看懂这种接口的作用)</p>
<p>索引签名可以设置为只读:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">ReadonlyStringArray</span> <span class="p">{</span>
<span class="k">readonly</span> <span class="p">[</span><span class="nx">index</span><span class="p">:</span> <span class="kr">number</span><span class="p">]:</span> <span class="kr">string</span>
<span class="p">}</span>
</code></pre>
<p>与 Java 一样,TypeScript 的接口里也可以定义一些方法:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">ClockInterface</span> <span class="p">{</span>
<span class="nl">currentTime</span><span class="p">:</span> <span class="nb">Date</span>
<span class="nx">setTime</span><span class="p">(</span><span class="nx">d</span><span class="p">:</span> <span class="nb">Date</span><span class="p">):</span> <span class="k">void</span>
<span class="p">}</span>
</code></pre>
<p>可以用带 <code>new()</code> 签名的接口来限制某个类的构造函数:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">ClockConstructor</span> <span class="p">{</span>
<span class="k">new</span><span class="p">(</span><span class="nx">hour</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">minute</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nx">ClockInterface</span>
<span class="p">}</span>
<span class="kr">interface</span> <span class="nx">ClockInterface</span> <span class="p">{</span>
<span class="nx">tick</span><span class="p">():</span> <span class="k">void</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">createClock</span><span class="p">(</span><span class="nx">ctor</span><span class="p">:</span> <span class="nx">ClockConstructor</span><span class="p">,</span> <span class="nx">hour</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">minute</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nx">ClockInterface</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">ctor</span><span class="p">(</span><span class="nx">hour</span><span class="p">,</span> <span class="nx">minute</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">DigitalClock</span> <span class="k">implements</span> <span class="nx">ClockInterface</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">h</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">m</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">tick</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">beep beep</span><span class="dl">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">AnalogClock</span> <span class="k">implements</span> <span class="nx">ClockInterface</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">h</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">m</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">tick</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">tick tock</span><span class="dl">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">digital</span> <span class="o">=</span> <span class="nx">createClock</span><span class="p">(</span><span class="nx">DigitalClock</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">17</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">analog</span> <span class="o">=</span> <span class="nx">createClock</span><span class="p">(</span><span class="nx">AnalogClock</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
</code></pre>
<p>与 Java 一样,TypeScritp 中的一个接口也可以继承多个其它接口。</p>
<p>JavaScript 中,一个 function 同时是一个对象,可以拥有其它属性,下面是一个 TypeScript 中的混合类型的示例:</p>
<pre class="highlight"><code class="language-typescript"><span class="kr">interface</span> <span class="nx">Counter</span> <span class="p">{</span>
<span class="p">(</span><span class="nx">start</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">string</span>
<span class="nx">interval</span><span class="p">:</span> <span class="kr">number</span>
<span class="nx">reset</span><span class="p">():</span> <span class="k">void</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">getCounter</span><span class="p">():</span> <span class="nx">Counter</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">counter</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">start</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span> <span class="k">as</span> <span class="nx">Counter</span>
<span class="nx">counter</span><span class="p">.</span><span class="nx">interval</span> <span class="o">=</span> <span class="mi">123</span>
<span class="nx">counter</span><span class="p">.</span><span class="nx">reset</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
<span class="k">return</span> <span class="nx">counter</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">getCounter</span><span class="p">()</span>
<span class="nx">c</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="nx">c</span><span class="p">.</span><span class="nx">reset</span><span class="p">()</span>
<span class="nx">c</span><span class="p">.</span><span class="nx">interval</span> <span class="o">=</span> <span class="mf">5.0</span>
</code></pre>
<p>TypeScript 中的接口可以继承类。当一个接口继承一个类时,该类中所有成员都会被继承,包括 protected 和 private 成员,但是没有具体的实现。这意味着该接口只能被这个类或它的子类实现。</p>
TypeScript
项目文件结构
tsconfig.json
{
"compilerOptions": {
"lib": ["es2015"],
"module": ...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1817
2022-11-26T09:41:18Z
2023-05-16T13:35:09Z
氧传感器相关知识笔记
<h2>
<a id="%E7%A9%BA%E7%87%83%E6%AF%94%E6%A6%82%E5%BF%B5" href="#%E7%A9%BA%E7%87%83%E6%AF%94%E6%A6%82%E5%BF%B5" class="anchor"></a>空燃比概念</h2>
<p>空燃比即空气与燃料的比例。汽油的理想空燃比 14.7 : 1,这里的比值是质量比,单位是 g。也就是说 1g 的汽油完全燃烧,需要 14.7g 的空气。比值越小,混合气体中的汽油占比越大,或者说混合气体偏浓;反之,比值越大,混合气体中的空气占比越大,或者说混合气体偏稀。</p>
<h2>
<a id="%E8%BF%87%E9%87%8F%E7%A9%BA%E6%B0%94%E7%B3%BB%E6%95%B0%EF%BC%88lambda%EF%BC%89" href="#%E8%BF%87%E9%87%8F%E7%A9%BA%E6%B0%94%E7%B3%BB%E6%95%B0%EF%BC%88lambda%EF%BC%89" class="anchor"></a>过量空气系数(lambda)</h2>
<p>由于其它燃料(柴油、氢气、天然气等)的理想空燃比并非 14.7 : 1,因此行业中引入了一个过量空气系数(lambda)。</p>
<p>lambda 是实际空燃比与理论空燃比的比值,由此可以推导出:当 lambda 为 1 时,为理想燃烧情况(也就是说在汽油机中,当 lambda 为 1 时,实际空燃比为 14:7 : 1);当 lambda < 1 时,表示混合气体偏浓;当 lambda > 1 时,表示混合气体偏稀。</p>
<p>当 lambda < 0.5 (过浓)或者 lambda > 1.4 (过稀)时,缸内的气体将无法点燃。</p>
<p>一般情况下,当发动机高功率运行时,lambda 值大约在 0.9 左右;当发动机运行于经济工况时,lambda 值大约在 1.1 左右。</p>
<h2>
<a id="%E6%8E%92%E6%94%BE" href="#%E6%8E%92%E6%94%BE" class="anchor"></a>排放</h2>
<p>混合气体燃烧,主要会产生三种有害物质:一氧化碳 CO,碳氢化合物 HC,氮氧化合物 NOx。</p>
<p><img src="/attachments/7W45mB3Rii69vnS5soKZF1p9/emission.png" alt="emission.png"></p>
<p>如图所示,在汽油机中,当空燃比为理想值 14.7 : 1(也就是 lambda 为 1)时,废气中 CO 和 HC 接近最低,但 NOx 的含量接近最高。但由于经过刻意的设计,此时三元催化器的转化效率是最高的,最终排出的尾气是最干净的(还有一个前提条件是温度达到 600 摄氏度左右)。</p>
<h2>
<a id="%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8%E7%9A%84%E4%BD%9C%E7%94%A8" href="#%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8%E7%9A%84%E4%BD%9C%E7%94%A8" class="anchor"></a>氧传感器的作用</h2>
<p>前氧传感器在三元催化器之前,可以通过发动机排出的空气中的含氧量来判断燃烧状态。将此信息反馈给 ECU,ECU 便可以不断地根据氧传感器返回的信息来调节喷油量,从而达到不同控制策略(巡航需要经济性、加速需要动力性等)下预期的燃烧效果。</p>
<p>后氧传感器在三元催化器之后,通过氧含量来监控三元催化器的工作效率。不过在现在一些新的车型上,后氧传感器也参与燃油修正。</p>
<p>诊断仪/软件当中经常用 Bank x Sensor y 表示传感器的编号。Bank 1 Sensor 1 表示第一列气缸的第一个传感器,通常是前氧传感器;以此类推 Bank 1 Sensor 2 表示第一列气缸的第二个传感器,通常是后氧传感器。而 Bank 2 Sensor 1 通常出现在 V 型发动机上,此类发动机有两列气缸,每列对应一个三元催化器,每个三元各有两个氧传感器。</p>
<h2>
<a id="%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86" href="#%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86" class="anchor"></a>氧传感器的工作原理</h2>
<p>氧化锆式传感器是比较常见的氧传感器,它的工作原理见下图:</p>
<p><img src="/attachments/wKc7pKPiToYp51zo95idEtFR/sensor.jpg" alt="sensor.jpg"></p>
<p>它的结构像一支试管,有内外两个面。将它插入排气管中之后,外侧的那个面直接与排气管中的废气接触,内侧一面则直接通着大气。“试管壁”中间层的材料就是二氧化锆,外侧和内侧分别包裹着一层铂。二氧化锆充当着电解质的角色,当温度足够高(> 350 摄氏度)的时候,它便会开始传递氧离子,大约在 650 摄氏度左右工作效率最高。包裹着二氧化锆内外两侧的两层铂,成为两个电极,这样就形成一个电池了(能斯特电池)。它的内侧通着大气,始终是富氧环境,含氧量始终固定(大气氧含量 21%);外侧含氧量就是尾气中的含氧量,是一直随着燃烧状况在变化的,相对内侧一定是更少的。因此大气一侧空气中的氧分子会失去两个正电荷,变成氧离子,往排气管一侧走,大气一侧成了正极,废气一侧成了负极,这样就有了电压。</p>
<p>跟三元催化器一样,氧传感器也怕铅和硫,这两种元素容易使它中毒。</p>
<p>当混合气体偏浓时,废气一侧含氧量相对少,大气侧与废气侧的电位差就比较大,因此产生的电压也比较大,但一般不会超过 0.9v;相反当混合气体偏稀时,废气一侧含氧量相对多,大气侧与废气侧的电位差比较小,产生的电压也比较小,一般最小低至 0.1v。</p>
<p><img src="/attachments/mukxJp8Hi3RS4cRWDDZuYYG7/o2sensor-voltage.png" alt="o2sensor-voltage.png"></p>
<p>如上图,二氧化锆有一个特性,就是达到工作温度之后,如果混合气体浓,废气侧无氧,大气侧氧离子就移动得很活跃。在混合气由浓变稀的过程中,氧离子的活跃程度几乎不怎么变化,表现为电压几乎一直维持在 0.9v 左右;直到过量空气系数 lambda 接近 1,废气侧开始逐渐有氧时,氧离子的活跃程度才开始下降,但是下降得很快,lambda 稍微超过 1 一些,氧离子就变得很不活跃了。因此此种氧传感器能够精确测量的,只有在 lambda 为 1 附近很窄的一段范围。也就是说,多数情况下,它只能知道混合气体浓了还是稀了,但具体浓了多少还是稀了多少无从得知——除非正好 lambda 值落在 1 附近。这种氧传感器叫作阶跃式氧传感器,又叫窄域氧传感器。</p>
<p>阶跃式氧传感器有四根线束,其中两根接着电极的两侧。负极接地,现在一般都不是在车身接地,而是在 ECU 内部有个虚拟的接地。并且有些车型会给一个参考电压,例如 1.5v,加上传感器本身的电压,测量到的结果就是电压在 1.6v ~ 2.4v 之间变化。不同的车型给的电压大小会有所不同。</p>
<p>氧传感器温度在 600 摄氏度以上时,电压变化的频率是每 10 秒 8 次。如果温度过低,这个频率会降低。早期的氧传感器只有一根线束,连接着正极信号线,负极直接搭铁。二氧化锆本身靠排气管加热。但是冷启动时,排气温度上来得慢,为了让氧传感器尽快进入工作状态,现在的氧传感器都加入了一个加热器,就又多了两根线束。加上铂电极的两根,就一共有四根线束。</p>
<p>加热器靠占空比控制输入电压的大小,从而调整自身的温度。加热器的电阻值一般在 10 欧以下,并且会随温度改变,温度升高,阻值也会升高。加热器温度普遍在 350 ~ 700 摄氏度左右。这个温度是一个估算值,并不是靠温度传感器检测到的真实温度。加热器最终产生的实际温度会使二氧化锆内阻也发生变化,ECU 会根据其产生的信号电压来判断温度,然后根据这个温度控制占空比,来调节加热器的温度。</p>
<h2>
<a id="%E5%AE%BD%E5%9F%9F%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8" href="#%E5%AE%BD%E5%9F%9F%E6%B0%A7%E4%BC%A0%E6%84%9F%E5%99%A8" class="anchor"></a>宽域氧传感器</h2>
<p>如果在二氧化锆两侧的电极人为地通上电,氧离子就会在电压的作用下运动,从电极的一侧移动到另一侧,这样就形成了一个“氧泵”,又叫泵氧元。通过改变电流的方向,可以控制氧离子的流向。</p>
<p>由此人们设计出了一种结构,可以放大阶跃式氧传感器的测量范围:</p>
<p><img src="/attachments/jUtYCHgsvcmiuJ6PDNo7gVcw/wide-range-sensor.jpg" alt="wide-range-sensor.jpg"></p>
<p>如图。既然二氧化锆只在有氧无氧的临界范围内对氧含量敏感,那么就想办法制造这么一个环境。于是人们把原先的氧传感器(2)靠近废气一侧的电极用一个腔室隔开(6),这个腔室叫取样室。别一侧仍然与大气接触(4)。取样室的一侧是能斯特电池(2)——即原先的传感器。另一侧是个氧泵(1)。氧泵的另一侧则与废气接触。同时取样室中有个扩散孔与废气接通,这样废气就能够不断地进入取样室。当能斯特电池(2)检测到混合气偏浓时,ECU 会通知氧泵(1)往取样室(6)内泵氧,直到能斯特电池(2)测得 lambda 为 1,也就是电压为 0.45v。相反,当能斯特电池(2)检测到混合气偏稀时,ECU 会通知氧泵(1)从取样室(6)往外抽氧,直到能斯特电池(2)测得 lambda 为 1。最后,通过计算氧泵在这段时间内通过的电流的流向和大小,就能得知废气浓了多少或是稀了多少。</p>
<p>由于多了个氧泵,原先阶越式氧传感器的四根线束在宽域氧传感器里就变成了六根线束。氧泵和能斯特电池可以共用一根接地线,因此也有五根线束的宽域氧传感器。</p>
<h2>
<a id="%E7%87%83%E6%B2%B9%E4%BF%AE%E6%AD%A3" href="#%E7%87%83%E6%B2%B9%E4%BF%AE%E6%AD%A3" class="anchor"></a>燃油修正</h2>
<p>ECU 在出厂的时候,会设定好一个基础的喷油量。喷出的油到气缸里燃烧之后,排出废气,被前氧传感器检测到。如果氧传感器电压 < 0.45v,检测到的结果就是混合气体偏稀,则 ECU 会增加喷油量,例如增加 2%;相反如果检测到混合气体偏浓,则 ECU 会尝试减少喷油量。下一次喷油量就是调整之后的量,如果还稀/浓了,ECU 就会再次调整。这样不断地根据前氧传感器返回的信号调整喷油量,就是“短期燃油修正”。</p>
<p>短期燃油修正稳定在某个范围一段时间之后,ECU 会把这个值写入存储器,覆盖原始的基本喷油量。之后短期燃油修正就在这个新的基础上进行。也就是说正常情况下,长期燃油修正写入之后,短期燃油修正应该是在 0 附近的范围上下轻微浮动。</p>
<p>当长期燃油修正超过极限值的时候(比如 25%),ECU 无法继续调整喷油量,就会报故障码:混合气体过稀(P0171)/浓(P0172)。</p>
空燃比概念
空燃比即空气与燃料的比例。汽油的理想空燃比 14.7 : 1,这里的比值是质量比,单位是 g。也就是说 1g 的汽油完全燃烧,需要 14.7g 的空气。比值越小,混合气体中的汽油占...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1512
2022-09-16T02:49:55Z
2022-10-28T15:03:44Z
利用 Paging 3 给 RecyclerView 列表分页
<h2>
<a id="%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86%EF%BC%9A" href="#%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86%EF%BC%9A" class="anchor"></a>前置知识:</h2>
<ul>
<li>Kotlin Flow 的使用;</li>
<li>Android 的 view binding;</li>
<li>RecyclerView 的基本用法。</li>
</ul>
<h2>
<a id="Paging+3+%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95" href="#Paging+3+%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95" class="anchor"></a>Paging 3 基本用法</h2>
<p>首先实现一个 <code>PagingSource<Key, Value></code> 类,这里的 <code>Key</code> 用于标识当前页,通常是当前的页码,<code>Value</code> 则是列表上每一项的具体内容对应的模型。例如:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">StuffPagingSource</span> <span class="p">:</span> <span class="nc">PagingSource</span><span class="p"><</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">StuffResponse</span><span class="p">>()</span>
</code></pre>
<p><code>PagingSource</code> 需要实现两个方法,一个是 <code>load()</code> 方法,用于从数据库或者远程 API 加载当前页所需要的数据。在该方法中,你主要需要做的是将当前页的页码、上一页的页码以及下一页的页码计算出来,并通过当前页的页码获取当前页的数据列表:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">StuffPagingSource</span> <span class="p">:</span> <span class="nc">PagingSource</span><span class="p"><</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">StuffResponse</span><span class="p">>()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">load</span><span class="p">(</span><span class="n">params</span><span class="p">:</span> <span class="nc">LoadParams</span><span class="p"><</span><span class="nc">Int</span><span class="p">>):</span> <span class="nc">LoadResult</span><span class="p"><</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">StuffResponse</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">page</span> <span class="p">=</span> <span class="n">params</span><span class="p">.</span><span class="n">key</span> <span class="o">?:</span> <span class="mi">1</span>
<span class="kd">val</span> <span class="py">pageSize</span> <span class="p">=</span> <span class="n">params</span><span class="p">.</span><span class="n">loadSize</span>
<span class="k">return</span> <span class="k">try</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="nc">StuffAPICaller</span><span class="p">.</span><span class="n">instance</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">pageSize</span><span class="p">)</span> <span class="c1">// 这里我是通过远程 API 获取数据</span>
<span class="kd">val</span> <span class="py">previousPage</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">previousPage</span> <span class="p">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">null</span> <span class="k">else</span> <span class="n">response</span><span class="p">.</span><span class="n">previousPage</span>
<span class="kd">val</span> <span class="py">nextPage</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">nextPage</span> <span class="p">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">null</span> <span class="k">else</span> <span class="n">response</span><span class="p">.</span><span class="n">nextPage</span>
<span class="nc">LoadResult</span><span class="p">.</span><span class="nc">Page</span><span class="p">(</span>
<span class="n">data</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">items</span><span class="p">,</span> <span class="c1">// response.items 类型为 List<StuffResponse></span>
<span class="n">prevKey</span> <span class="p">=</span> <span class="n">previousPage</span><span class="p">,</span>
<span class="n">nextKey</span> <span class="p">=</span> <span class="n">nextPage</span>
<span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">exception</span><span class="p">:</span> <span class="nc">HttpException</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">LoadResult</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">exception</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">exception</span><span class="p">:</span> <span class="nc">IOException</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">LoadResult</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">exception</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>这里 <code>LoadResult.Page</code> 的构造参数里,如果当前页是第 1 页,<code>prevKey</code> 必须是 <code>null</code> 而不能是其它值;如果当前页是最后一页,则 <code>nextKey</code> 必须是 <code>null</code>. 而 <code>data</code> 的类型是你最终要在 UI 上使用的类型的 List.</p>
<p>尾部的两个 <code>LoadResult.Error</code> 最终会被传递给 <code>LoadStateAdapter</code> 处理,这个后面会提到。</p>
<p>另外 <code>PagingSource</code> 需要实现一个 <code>getRefreshKey()</code> 方法,具体的作用还没看太明白,似乎逻辑就是由当前锚点定位到当前页码,复制一段官方示例代码:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">override</span> <span class="k">fun</span> <span class="nf">getRefreshKey</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="nc">PagingState</span><span class="p"><</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">Item</span><span class="p">>):</span> <span class="nc">Int</span><span class="p">?</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">state</span><span class="p">.</span><span class="n">anchorPosition</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
<span class="n">state</span><span class="p">.</span><span class="nf">closestPageToPosition</span><span class="p">(</span><span class="n">it</span><span class="p">)</span><span class="o">?.</span><span class="n">prevKey</span><span class="o">?.</span><span class="nf">plus</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="o">?:</span> <span class="n">state</span><span class="p">.</span><span class="nf">closestPageToPosition</span><span class="p">(</span><span class="n">it</span><span class="p">)</span><span class="o">?.</span><span class="n">nextKey</span><span class="o">?.</span><span class="nf">minus</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>读了一遍逻辑,起码在我的代码里直接用没什么问题。</p>
<p>在对应的 ViewModel 里使用:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">StuffListViewModel</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">getPagingDataFlow</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">PagingData</span><span class="p"><</span><span class="nc">StuffResponse</span><span class="p">>></span> <span class="p">{</span>
<span class="k">return</span> <span class="nc">Pager</span><span class="p">(</span>
<span class="n">config</span> <span class="p">=</span> <span class="nc">PagingConfig</span><span class="p">(</span><span class="n">pageSize</span> <span class="p">=</span> <span class="mi">15</span><span class="p">,</span> <span class="n">initialLoadSize</span> <span class="p">=</span> <span class="mi">15</span><span class="p">),</span>
<span class="n">pagingSourceFactory</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">StuffPagingSource</span><span class="p">()</span> <span class="p">}</span>
<span class="p">).</span><span class="n">flow</span><span class="p">.</span><span class="nf">cachedIn</span><span class="p">(</span><span class="n">viewModelScope</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>这段代码里 <code>PagingConfig</code> 构造参数中的 <code>initialLoadSize</code> 用于配置加载第一页数据时的数据量,默认是 <code>pageSize</code> 的 3 倍。具体数值设置为多少合适,需要配合前面 <code>PagingSource.load()</code> 方法中 <code>pageSize</code> 变量的获取/计算方式来决定。</p>
<p>再将原来 <code>RecyclerView.Adapter</code> 的实现替换为继承 <code>PagingDataAdapter</code>, 此时需要传递一个 <code>DiffUtil.ItemCallback</code> 对象给 <code>PagingDataAdapter</code> 的构造方法。<code>getItemCount()</code> 方法则不需要了。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">StuffListAdapter</span> <span class="p">:</span> <span class="nc">PagingDataAdapter</span><span class="p"><</span><span class="nc">StuffResponse</span><span class="p">,</span> <span class="nc">StuffListAdapter</span><span class="p">.</span><span class="nc">ListItemViewHolder</span><span class="p">>(</span><span class="nc">COMPARATOR</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">object</span> <span class="nc">COMPARATOR</span> <span class="p">:</span> <span class="nc">DiffUtil</span><span class="p">.</span><span class="nc">ItemCallback</span><span class="p"><</span><span class="nc">StuffResponse</span><span class="p">>()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">areItemsTheSame</span><span class="p">(</span><span class="n">oldItem</span><span class="p">:</span> <span class="nc">StuffResponse</span><span class="p">,</span> <span class="n">newItem</span><span class="p">:</span> <span class="nc">StuffResponse</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">oldItem</span><span class="p">.</span><span class="n">id</span> <span class="p">==</span> <span class="n">newItem</span><span class="p">.</span><span class="n">id</span>
<span class="p">}</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">areContentsTheSame</span><span class="p">(</span><span class="n">oldItem</span><span class="p">:</span> <span class="nc">StuffResponse</span><span class="p">,</span> <span class="n">newItem</span><span class="p">:</span> <span class="nc">StuffResponse</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">oldItem</span><span class="p">.</span><span class="n">content</span> <span class="p">==</span> <span class="n">newItem</span><span class="p">.</span><span class="n">content</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onBindViewHolder</span><span class="p">(</span><span class="n">holder</span><span class="p">:</span> <span class="nc">ListItemViewHolder</span><span class="p">,</span> <span class="n">position</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreateViewHolder</span><span class="p">(</span><span class="n">parent</span><span class="p">:</span> <span class="nc">ViewGroup</span><span class="p">,</span> <span class="n">viewType</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">ListItemViewHolder</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">ListItemViewHolder</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">binding</span><span class="p">:</span> <span class="nc">StuffListItemBinding</span><span class="p">)</span> <span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span><span class="p">(</span><span class="n">binding</span><span class="p">.</span><span class="n">root</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>最后在 Fragment/Activity 上使用:</p>
<pre class="highlight"><code class="language-kotlin"><span class="n">lifecycleScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
<span class="n">viewModel</span><span class="p">.</span><span class="nf">getPagingDataFlow</span><span class="p">().</span><span class="nf">collectLatest</span> <span class="p">{</span> <span class="n">pagingData</span> <span class="p">-></span>
<span class="n">adapter</span><span class="p">.</span><span class="nf">submitData</span><span class="p">(</span><span class="n">pagingData</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<h2>
<a id="%E6%98%BE%E7%A4%BA%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81" href="#%E6%98%BE%E7%A4%BA%E6%95%B0%E6%8D%AE%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81" class="anchor"></a>显示数据加载状态</h2>
<p>前面的 <code>PagingSource.load()</code> 方法在捕获异常之后,返回了两个 <code>LoadResult.Error</code> 对象。在某些情况下(例如网络断开、服务器关机)时,会引发相关异常,这些异常信息可以显示在列表的最底部,并同时提供一个按钮供用户尝试重新加载。另外当网络拥堵时,数据加载过慢,也可以在列表最底部显示一个旋转的进度条表示数据正在加载。</p>
<p>首先需要一个界面来显示这些内容,我起名为 <code>load_state_list_item.xml</code>:</p>
<pre class="highlight"><code class="language-xml"><span class="cp"><?xml version="1.0" encoding="utf-8"?></span>
<span class="nt"><RelativeLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_margin=</span><span class="s">"16dp"</span><span class="nt">></span>
<span class="nt"><TextView</span>
<span class="na">android:id=</span><span class="s">"@+id/error_message_text_view"</span>
<span class="na">android:layout_width=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_centerVertical=</span><span class="s">"true"</span>
<span class="na">android:layout_alignParentStart=</span><span class="s">"true"</span>
<span class="na">android:textAppearance=</span><span class="s">"@style/TextAppearance.AppCompat.Body2"</span>
<span class="na">android:textColor=</span><span class="s">"@color/design_default_color_error"</span>
<span class="na">android:textSize=</span><span class="s">"16sp"</span> <span class="nt">/></span>
<span class="nt"><ProgressBar</span>
<span class="na">android:id=</span><span class="s">"@+id/progress_bar"</span>
<span class="na">android:layout_width=</span><span class="s">"60dp"</span>
<span class="na">android:layout_height=</span><span class="s">"60dp"</span>
<span class="na">android:layout_centerHorizontal=</span><span class="s">"true"</span>
<span class="na">android:layout_centerVertical=</span><span class="s">"true"</span><span class="nt">/></span>
<span class="nt"><Button</span>
<span class="na">android:id=</span><span class="s">"@+id/retry_button"</span>
<span class="na">android:layout_width=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_alignParentEnd=</span><span class="s">"true"</span>
<span class="na">android:layout_centerVertical=</span><span class="s">"true"</span>
<span class="na">android:text=</span><span class="s">"@string/button_page_loading_retry"</span> <span class="nt">/></span>
<span class="nt"></RelativeLayout></span>
</code></pre>
<p>接着实现一个 <code>LoadStateAdapter</code> 以及它的 <code>ViewHolder</code></p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">LoadStateFooterAdapter</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">retry</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">:</span> <span class="nc">LoadStateAdapter</span><span class="p"><</span><span class="nc">LoadStateFooterAdapter</span><span class="p">.</span><span class="nc">LoadStateViewHolder</span><span class="p">>()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onBindViewHolder</span><span class="p">(</span><span class="n">holder</span><span class="p">:</span> <span class="nc">LoadStateViewHolder</span><span class="p">,</span> <span class="n">loadState</span><span class="p">:</span> <span class="nc">LoadState</span><span class="p">)</span> <span class="p">{</span>
<span class="n">holder</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="n">loadState</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreateViewHolder</span><span class="p">(</span><span class="n">parent</span><span class="p">:</span> <span class="nc">ViewGroup</span><span class="p">,</span> <span class="n">loadState</span><span class="p">:</span> <span class="nc">LoadState</span><span class="p">)</span> <span class="p">:</span> <span class="nc">LoadStateViewHolder</span> <span class="p">{</span>
<span class="c1">// 这里我开启了 view binding</span>
<span class="kd">val</span> <span class="py">binding</span> <span class="p">=</span> <span class="nc">LoadStateListItemBinding</span><span class="p">.</span><span class="nf">inflate</span><span class="p">(</span><span class="nc">LayoutInflater</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="n">parent</span><span class="p">.</span><span class="n">context</span><span class="p">),</span> <span class="n">parent</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span>
<span class="k">return</span> <span class="nc">LoadStateViewHolder</span><span class="p">(</span><span class="n">binding</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">LoadStateViewHolder</span><span class="p">(</span><span class="kd">val</span> <span class="py">binding</span><span class="p">:</span> <span class="nc">LoadStateListItemBinding</span><span class="p">)</span> <span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span><span class="p">(</span><span class="n">binding</span><span class="p">.</span><span class="n">root</span><span class="p">)</span> <span class="p">{</span>
<span class="k">fun</span> <span class="nf">bind</span><span class="p">(</span><span class="n">loadState</span><span class="p">:</span> <span class="nc">LoadState</span><span class="p">)</span> <span class="p">{</span>
<span class="n">binding</span><span class="p">.</span><span class="n">progressBar</span><span class="p">.</span><span class="n">isVisible</span> <span class="p">=</span> <span class="n">loadState</span> <span class="k">is</span> <span class="nc">LoadState</span><span class="p">.</span><span class="nc">Loading</span>
<span class="n">binding</span><span class="p">.</span><span class="n">retryButton</span><span class="p">.</span><span class="n">isVisible</span> <span class="p">=</span> <span class="n">loadState</span> <span class="p">!</span><span class="k">is</span> <span class="nc">LoadState</span><span class="p">.</span><span class="nc">Loading</span>
<span class="n">binding</span><span class="p">.</span><span class="n">errorMessageTextView</span><span class="p">.</span><span class="n">isVisible</span> <span class="p">=</span> <span class="n">loadState</span> <span class="p">!</span><span class="k">is</span> <span class="nc">LoadState</span><span class="p">.</span><span class="nc">Loading</span>
<span class="k">if</span> <span class="p">(</span><span class="n">loadState</span> <span class="k">is</span> <span class="nc">LoadState</span><span class="p">.</span><span class="nc">Error</span><span class="p">)</span> <span class="p">{</span>
<span class="n">binding</span><span class="p">.</span><span class="n">errorMessageTextView</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">loadState</span><span class="p">.</span><span class="n">error</span><span class="p">.</span><span class="n">localizedMessage</span>
<span class="p">}</span>
<span class="n">binding</span><span class="p">.</span><span class="n">retryButton</span><span class="p">.</span><span class="nf">setOnClickListener</span> <span class="p">{</span>
<span class="p">(</span><span class="n">bindingAdapter</span> <span class="k">as</span> <span class="nc">LoadStateFooterAdapter</span><span class="p">).</span><span class="n">retry</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>然后在原来给 RecyclerView 的 adapter 赋值的地方,给 adapter 加上 load state footer 即可。</p>
<pre class="highlight"><code class="language-kotlin"><span class="n">recyclerView</span><span class="p">.</span><span class="nf">let</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="n">it</span><span class="p">.</span><span class="n">adapter</span> <span class="p">=</span> <span class="n">adapter</span><span class="p">.</span><span class="nf">withLoadStateFooter</span><span class="p">(</span><span class="nc">LoadStateFooterAdapter</span><span class="p">(</span><span class="n">adapter</span><span class="o">::</span><span class="n">retry</span><span class="p">))</span>
<span class="p">}</span>
</code></pre>
<p>当然如果你希望加在头部可以使用 <code>withLoadStateHeader()</code>, 如果想首尾都加,可以使用 <code>withLoadStateHeaderAndFooter()</code>. 代码当中的 <code>adapter::retry</code> 来自 <code>PagingDataAdapter</code> 本身,无需实现。</p>
<h2>
<a id="%E5%B7%A6%E5%8F%B3%E6%BB%91%E5%8A%A8%E5%AE%9E%E7%8E%B0%E5%88%A0%E9%99%A4" href="#%E5%B7%A6%E5%8F%B3%E6%BB%91%E5%8A%A8%E5%AE%9E%E7%8E%B0%E5%88%A0%E9%99%A4" class="anchor"></a>左右滑动实现删除</h2>
<pre class="highlight"><code class="language-kotlin"><span class="k">abstract</span> <span class="kd">class</span> <span class="nc">SwipeCallback</span><span class="p">(</span><span class="n">direction</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="nc">ItemTouchHelper</span><span class="p">.</span><span class="nc">RIGHT</span> <span class="n">or</span> <span class="nc">ItemTouchHelper</span><span class="p">.</span><span class="nc">LEFT</span><span class="p">)</span>
<span class="p">:</span> <span class="nc">ItemTouchHelper</span><span class="p">.</span><span class="nc">SimpleCallback</span><span class="p">(</span><span class="nc">ItemTouchHelper</span><span class="p">.</span><span class="nc">ACTION_STATE_IDLE</span><span class="p">,</span> <span class="n">direction</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 用于实现拖拽排序之类的逻辑,这里暂时用不着。</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onMove</span><span class="p">(</span>
<span class="n">recyclerView</span><span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">,</span>
<span class="n">viewHolder</span><span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span><span class="p">,</span>
<span class="n">target</span><span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span>
<span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">false</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">StuffListViewModel</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">fun</span> <span class="nf">deleteStuff</span><span class="p">(</span><span class="n">stuff</span><span class="p">:</span> <span class="nc">StuffModel</span><span class="p">,</span> <span class="n">callback</span><span class="p">:</span> <span class="p">()</span> <span class="p">-></span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
<span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
<span class="n">stuff</span><span class="p">.</span><span class="nf">delete</span><span class="p">()</span>
<span class="n">callback</span><span class="p">.</span><span class="nf">invoke</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">StuffListFragment</span> <span class="p">:</span> <span class="nc">Fragment</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">adapter</span> <span class="p">=</span> <span class="nc">StuffListAdapter</span><span class="p">()</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onViewCreated</span><span class="p">(</span><span class="n">view</span><span class="p">:</span> <span class="nc">View</span><span class="p">,</span> <span class="n">savedInstanceState</span><span class="p">:</span> <span class="nc">Bundle</span><span class="p">?)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">.</span><span class="nf">onViewCreated</span><span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="n">savedInstanceState</span><span class="p">)</span>
<span class="c1">// ......</span>
<span class="nc">ItemTouchHelper</span><span class="p">(</span><span class="kd">object</span> <span class="err">: </span><span class="nc">SwipeCallback</span><span class="p">()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onSwiped</span><span class="p">(</span><span class="n">viewHolder</span><span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span><span class="p">,</span> <span class="n">direction</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">stuff</span> <span class="p">=</span> <span class="p">(</span><span class="n">viewHolder</span> <span class="k">as</span> <span class="nc">StuffListAdapter</span><span class="p">.</span><span class="nc">ListItemViewHolder</span><span class="p">).</span><span class="n">stuff</span>
<span class="c1">// 这里如果不用 callback 的形式传入 refresh 方法而是直接在下一行调用</span>
<span class="c1">// deleteStuff() 会和 adapter.refresh() 并发执行,UI 的行为就会变得很奇怪</span>
<span class="n">viewModel</span><span class="p">.</span><span class="nf">deleteStuff</span><span class="p">(</span><span class="n">stuff</span><span class="p">,</span> <span class="n">adapter</span><span class="o">::</span><span class="n">refresh</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}).</span><span class="nf">attachToRecyclerView</span><span class="p">(</span><span class="n">recyclerView</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>这里 <code>onSwiped()</code> 方法起初的实现是这样的:</p>
<pre class="highlight"><code class="language-kotlin"><span class="nc">ItemTouchHelper</span><span class="p">(</span><span class="kd">object</span> <span class="err">: </span><span class="nc">SwipeCallback</span><span class="p">()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">onSwiped</span><span class="p">(</span><span class="n">viewHolder</span><span class="p">:</span> <span class="nc">RecyclerView</span><span class="p">.</span><span class="nc">ViewHolder</span><span class="p">,</span> <span class="n">direction</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">stuff</span> <span class="p">=</span> <span class="p">(</span><span class="n">viewHolder</span> <span class="k">as</span> <span class="nc">StuffListAdapter</span><span class="p">.</span><span class="nc">ListItemViewHolder</span><span class="p">).</span><span class="n">stuff</span>
<span class="n">viewModel</span><span class="p">.</span><span class="nf">deleteStuff</span><span class="p">(</span><span class="n">stuff</span><span class="p">)</span>
<span class="n">adapter</span><span class="p">.</span><span class="nf">notifyItemRemoved</span><span class="p">(</span><span class="n">viewHolder</span><span class="p">.</span><span class="n">adapterBindingPosition</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}).</span><span class="nf">attachToRecyclerView</span><span class="p">(</span><span class="n">recyclerView</span><span class="p">)</span>
</code></pre>
<p>但由于用了 Paging 3, 采用这种实现会出现各种奇怪的问题,例如在第 1 页删除掉某一项,向下滚动页面,之后再滚回来,会发现删除掉的那一项还在。<a href="https://developer.android.com/reference/kotlin/androidx/paging/PagingSource#updating-data">官方的解释是</a>:</p>
<blockquote>
<p>A PagingSource / PagingData pair is a snapshot of the data set. A new PagingData / PagingData must be created if an update occurs, such as a reorder, insert, delete, or content update occurs. A PagingSource must detect that it cannot continue loading its snapshot (for instance, when Database query notices a table being invalidated), and call invalidate. Then a new PagingSource / PagingData pair would be created to represent data from the new state of the database query.</p>
</blockquote>
<p>意思是当 PagingData 的内容发生变化时(包括重新排序、插入新项、删除某一项),要通过调用 <code>PagingSource</code> 的 <code>invalidated()</code> 方法来使旧的 PagingSource 和 PagingData 失效,同时创建新的 <code>PagingSource</code> 实例来加载数据。不过我试了一下,直接调用 adapter 的 <code>refresh()</code> 方法也能有同样的效果。</p>
<p>这里同时也解决了我以前尝试实现拖拽排序时对各种诡异现象的疑惑。</p>
前置知识:
Kotlin Flow 的使用;
Android 的 view binding;
RecyclerView 的基本用法。
Paging 3 基本用法
首先实现一个 Pagin...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1484
2022-09-08T04:03:53Z
2022-10-28T15:03:12Z
Svelte props or component events?
<p>在 Svelte 中,一个组件的事件具体行为如果必须由外部来定义的话,有两种解决办法,一种是用 props, 一种是组件的自定义事件。</p>
<p>使用 props 的写法:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Hamburger</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">alertName</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">name=</span><span class="s">{name}</span> <span class="na">sayMyName=</span><span class="s">{alertName}/</span><span class="nt">></span>
<span class="c"><!-- Inner.svelte --></span>
<span class="nt"><script></span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">name</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">sayMyName</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><form</span> <span class="na">on:submit</span><span class="err">|</span><span class="na">preventDefault=</span><span class="s">{sayMyName}</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">bind:value=</span><span class="s">{name}/</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'submit'</span><span class="nt">></span>Say it!<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre>
<p>使用 component events 的写法:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">alertName</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">on:say=</span><span class="s">{alertName}/</span><span class="nt">></span>
<span class="c"><!-- Inner.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createEventDispatcher</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Hamburger</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nx">createEventDispatcher</span><span class="p">();</span>
<span class="kd">function</span> <span class="nx">sayMyName</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">dispatch</span><span class="p">(</span><span class="dl">'</span><span class="s1">say</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span><span class="nx">name</span><span class="p">});</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><form</span> <span class="na">on:submit</span><span class="err">|</span><span class="na">preventDefault=</span><span class="s">{sayMyName}</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">bind:value=</span><span class="s">{name}/</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'submit'</span><span class="nt">></span>Say it!<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre>
<p>这里边很大的一个区别,是数据传递的方式。前者是将定义在外部 <code><App/></code> 的 <code>name</code> 作为 <code><Inner/></code> 组件的属性值传递到内部;后者则是将 <code><Inner/></code> 内部的属性通过 <code>event.detail</code> 暴露给外部。在多数情况下,这两种方法没有太大区别。我们很容易首选 <code>props</code> 的方案,因为从代码上看,明显这种方式写起来更方便,也更符合我们的直觉。我们都知道 JavaScript 里的 function 是可以被赋值给任意变量的。</p>
<p>但是当这个属性是个数组或其它对象时,二者的表现就可能很不一样,下面用一个数组来演示:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">children</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">function</span> <span class="nx">count</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="s2">`I have </span><span class="p">${</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> children!`</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">children=</span><span class="s">{children}</span> <span class="na">countChildren=</span><span class="s">{count}/</span><span class="nt">></span>
<span class="c"><!-- Inner.svelte --></span>
<span class="nt"><script></span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">children</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">countChildren</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">addChild</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">children</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">children</span><span class="p">,</span> <span class="p">{}];</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><form</span> <span class="na">on:submit</span><span class="err">|</span><span class="na">preventDefault=</span><span class="s">{countChildren}</span><span class="nt">></span>
<span class="nt"><div></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'button'</span> <span class="na">on:click=</span><span class="s">{addChild}</span><span class="nt">></span>Add<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"><ul></span>
{#each children as child}
<span class="nt"><li><input</span> <span class="na">bind:value=</span><span class="s">{child.name}/</span><span class="nt">></li></span>
{/each}
<span class="nt"></ul></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'submit'</span><span class="nt">></span>Count<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre>
<p><code><App/></code> 把 <code>children</code> 和 <code>count()</code> 函数都当作 props 传递给了 <code><Inner/></code>. 这种情况下,我们期望的效果是:点击 3 次 Add 按钮之后,再点击 Count 按钮,会弹出一个 alert 框,显示 "I have 3 children!". 但结果显示的却是 "I have 0 children!".</p>
<p>问题出在 <code>addChild()</code> 这个函数。经过 Svelte 基础知识的学习,我们知道对于非基本类型的对象以及数组,要触发它的响应性(改变值时界面跟着变化),不能期望通过直接操作这个变量的方法来触发,而应该对该变量或者该变量的属性进行赋值操作。在 <code>addChild()</code> 这个例子里,就应该使用 <code>children = [...children, {}]</code> 的写法,而非 <code>children.push({})</code>.</p>
<p>另一方面,同时我们也知道,将响应性的变量赋值给另一个变量时,通过第 2 个变量来改变其属性,将不会触发响应性。在该例子当中,<code><App/></code> 的 <code>children</code> 赋值给 <code><Inner/></code> 之后,<code>addChild()</code> 函数操作的是 <code><Inner/></code> 内部的 <code>children</code>, 因此不会引起 <code><App/></code> 中 <code>children</code> 的变化,所以就有了我们看到的结果。</p>
<p>这时候,如果使用 component events, 将 <code>children</code> 作为 <code><Inner/></code> 内部的变量,通过自定义事件的方式传递到 <code><App/></code> 就可以避免这个问题:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">count</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="s2">`I have </span><span class="p">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> children!`</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">on:count=</span><span class="s">{count}/</span><span class="nt">></span>
<span class="c"><!-- Inner --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createEventDispatcher</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">children</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nx">createEventDispatcher</span><span class="p">();</span>
<span class="kd">function</span> <span class="nx">addChild</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">children</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">children</span><span class="p">,</span> <span class="p">{}];</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">countChildren</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">dispatch</span><span class="p">(</span><span class="dl">'</span><span class="s1">count</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span><span class="nx">children</span><span class="p">});</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><form</span> <span class="na">on:submit</span><span class="err">|</span><span class="na">preventDefault=</span><span class="s">{countChildren}</span><span class="nt">></span>
<span class="nt"><div></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'button'</span> <span class="na">on:click=</span><span class="s">{addChild}</span><span class="nt">></span>Add<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"><ul></span>
{#each children as child}
<span class="nt"><li><input</span> <span class="na">bind:value=</span><span class="s">{child.name}/</span><span class="nt">></li></span>
{/each}
<span class="nt"></ul></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">'submit'</span><span class="nt">></span>Count<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre>
在 Svelte 中,一个组件的事件具体行为如果必须由外部来定义的话,有两种解决办法,一种是用 props, 一种是组件的自定义事件。
使用 props 的写法:
<!-- App.sve...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1472
2022-08-26T07:02:47Z
2022-10-28T15:02:59Z
Android 网络请求绕过 HTTPS 限制
<p>targetSDK 升 28 之后,Android 强制要求网络请求必须使用 https 协议。在公网服务器上这事情好办,直接开启 https 支持即可。而且现在也没有什么网站是不支持 https 的了吧。</p>
<p>但在自己的电脑上开发和调试 Android 程序时,这个限制就不太方便。可能 Google 也考虑到了这一点,因此<a href="https://developer.android.google.cn/codelabs/android-network-security-config">提供了一个方法</a>。</p>
<p>首先开启本地服务器,假设其运行在 3000 端口上。</p>
<p>新建一个 res/xml/network_security_config.xml 文件,并在 AndroidManifest.xml 当中引用它:</p>
<pre class="highlight"><code class="language-xml"><span class="cp"><?xml version="1.0" encoding="utf-8"?></span>
<span class="c"><!-- AndroidManifest.xml --></span>
<span class="nt"><manifest</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
<span class="na">package=</span><span class="s">"your.package.name.here>
<application
android:networkSecurityConfig="</span><span class="err">@xml/network_security_config"</span>
<span class="err">....</span><span class="nt">></span>
<span class="nt"><activity</span> <span class="err">...</span><span class="nt">/></span>
<span class="nt"></manifest></span>
</code></pre>
<p>network_security_config.xml 内容如下:</p>
<pre class="highlight"><code class="language-xml"><span class="cp"><?xml version="1.0" encoding="utf-8"?></span>
<span class="c"><!-- network_security_config.xml --></span>
<span class="nt"><network-security-config></span>
<span class="nt"><domain-config</span> <span class="na">cleartextTrafficPermitted=</span><span class="s">"true"</span><span class="nt">></span>
<span class="c"><!-- 这里配置上域名白名单 --></span>
<span class="nt"><domain</span> <span class="na">includeSubdomains=</span><span class="s">"true"</span><span class="nt">></span>localhost<span class="nt"></domain></span>
<span class="nt"></domain-config></span>
<span class="nt"></network-security-config></span>
</code></pre>
<p>然后把手机的 3000 端口映射到开发机的 3000 端口。</p>
<pre class="highlight"><code class="language-shell">adb reverse tcp:3000 tcp:3000
</code></pre>
<p>最后,将你代码中访问服务的域名改为 localhost 即可。</p>
<p>补充:虽然非常不建议,但如果你想让整个 APP 访问任何网络都不受这个 https 协议的限制,可以这么做:</p>
<pre class="highlight"><code class="language-xml"><span class="cp"><?xml version="1.0" encoding="utf-8"?></span>
<span class="nt"><network-security-config></span>
<span class="nt"><base-config</span> <span class="na">cleartextTrafficPermitted=</span><span class="s">"true"</span><span class="nt">></base-config></span>
<span class="nt"></network-security-config></span>
</code></pre>
targetSDK 升 28 之后,Android 强制要求网络请求必须使用 https 协议。在公网服务器上这事情好办,直接开启 https 支持即可。而且现在也没有什么网站是不支持 http...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1471
2022-08-26T06:58:52Z
2022-10-28T15:02:59Z
在 Kotlin 中代理成员变量的方法、属性
<p>有如下代码</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">A</span> <span class="p">{</span>
<span class="k">fun</span> <span class="nf">fun1</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"fun 1 in A"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">B</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">a</span> <span class="p">=</span> <span class="nc">A</span><span class="p">()</span>
<span class="p">}</span>
</code></pre>
<p>如果你想在类 <code>B</code> 里实现一个方法 <code>fun1()</code> 交给 <code>a</code> 代理, 最简单的办法是:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">B</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">a</span> <span class="p">=</span> <span class="nc">A</span><span class="p">()</span>
<span class="k">fun</span> <span class="nf">fun1</span><span class="p">()</span> <span class="p">{</span>
<span class="n">a</span><span class="p">.</span><span class="nf">fun1</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>但是需要代理的方法有很多,会出现许多重复的样板代码。Kotlin 提供了一个办法来简单的实现代理,但是你得首先声明一个接口,这个接口中要定义好所有你想代理的方法,同时让 <code>A</code> 和 <code>B</code> 都实现该接口。因为 <code>B</code> 需要 <code>A</code> 代理这些方法,所以 <code>A</code> 和 <code>B</code> 都有同样的方法,所以实现同一个接口挺合理:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">interface</span> <span class="nc">Fun1</span> <span class="p">{</span>
<span class="k">fun</span> <span class="nf">fun1</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">A</span> <span class="p">:</span> <span class="nc">Fun1</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">fun1</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"fun 1 in A"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">B</span> <span class="p">:</span> <span class="nc">Fun1</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">a</span> <span class="p">=</span> <span class="nc">A</span><span class="p">()</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">fun1</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>然后把 <code>B</code> 的方法代理给 <code>a</code>, 需要把成员变量 <code>a</code> 定义在构造方法的参数里,否则访问不到 <code>a</code>:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">B</span><span class="p">(</span><span class="kd">val</span> <span class="py">a</span><span class="p">:</span> <span class="nc">A</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Fun1</span> <span class="k">by</span> <span class="n">a</span>
</code></pre>
<p>当然 <code>a</code> 也可以不是成员变量,不过这样似乎没什么意义:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">B</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nc">A</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Fun1</span> <span class="k">by</span> <span class="n">a</span>
</code></pre>
<p>这样就完成了。同样,属性也可以被代理:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">interface</span> <span class="nc">PageMeta</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">totalPages</span><span class="p">:</span> <span class="nc">Int</span>
<span class="kd">val</span> <span class="py">currentPage</span><span class="p">:</span> <span class="nc">Int</span>
<span class="p">}</span>
<span class="kd">data class</span> <span class="nc">BasePageMeta</span><span class="p">(</span>
<span class="kd">val</span> <span class="py">count</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="kd">val</span> <span class="py">previousPage</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="kd">val</span> <span class="py">nextPage</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="k">override</span> <span class="kd">val</span> <span class="py">totalPages</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="k">override</span> <span class="kd">val</span> <span class="py">currentPage</span><span class="p">:</span> <span class="nc">Int</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">PageMeta</span>
<span class="kd">data class</span> <span class="nc">PagedStuffListResponse</span><span class="p">(</span>
<span class="kd">val</span> <span class="py">meta</span><span class="p">:</span> <span class="nc">BasePageMeta</span><span class="p">,</span>
<span class="c1">// ...</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">PageMeta</span> <span class="k">by</span> <span class="n">meta</span>
</code></pre>
有如下代码
class A {
fun fun1() {
println("fun 1 in A")
}
}
class B {
val a = A()...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1431
2022-08-11T03:43:43Z
2022-10-28T15:02:11Z
SvelteJS 笔记(上)
<p><em>Svelte 官方在线编辑和运行代码的工具:<a href="https://svelte.dev/repl/hello-world?version=3.49.0">https://svelte.dev/repl/hello-world?version=3.49.0</a></em></p>
<h2>
<a id="%E7%BB%84%E4%BB%B6+%28Component%29" href="#%E7%BB%84%E4%BB%B6+%28Component%29" class="anchor"></a>组件 (Component)</h2>
<p>在 Svelte 当中,一个 .svelte 文件就是一个组件。.svelte 文件中可以包含 HTML, CSS 和 JavaScript.</p>
<p>在组件的 HTML 里可以直接引用 JavaScript 当中定义的变量(即组件的状态)。</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">world</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">/tutorial/image.gif</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><h1></span>Hello {name}!<span class="nt"></h1></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">{src}</span> <span class="na">alt=</span><span class="s">'image'</span><span class="nt">/></span>
</code></pre>
<p>当 HTML 标签的属性名跟变量名一样的时候,还可以简写:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><img</span> <span class="err">{</span><span class="na">src</span><span class="err">}</span> <span class="na">alt=</span><span class="s">'image'</span><span class="nt">/></span>
</code></pre>
<p>在组件里可以通过 CSS 定义样式:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><p></span>This is a paragraph.<span class="nt"></p></span>
<span class="nt"><style></span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">purple</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s2">'Comic Sans MS'</span><span class="p">,</span> <span class="nb">cursive</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">2em</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<p>这里的样式只在当前组件范围内有效。</p>
<p>组件是可以嵌套的:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Nested</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Nested.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><p></span>This is a paragraph.<span class="nt"></p></span>
<span class="nt"><Nested/></span>
<span class="c"><!-- 文件 Nested.svelte --></span>
<span class="nt"><p></span>This is another paragraph.<span class="nt"></p></span>
<span class="nt"><style></span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">purple</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s2">'Comic Sans MS'</span><span class="p">,</span> <span class="nb">cursive</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">2em</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<p>有时候变量里带有 HTML 标签,默认情况下,它是以字符串形式直接将 HTML 原始内容显示在界面上的。如果你想显示渲染后的效果,可以在访问变量的时候加上 <code>@html</code>.</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">string</span> <span class="o">=</span> <span class="s2">`this string contains some <strong>HTML!!!</strong>`</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><p></span>{@html string}<span class="nt"></p></span>
</code></pre>
<h2>
<a id="%E5%93%8D%E5%BA%94%E6%80%A7+%28Reactivity%29" href="#%E5%93%8D%E5%BA%94%E6%80%A7+%28Reactivity%29" class="anchor"></a>响应性 (Reactivity)</h2>
<p>组件的状态变量在变化时,会同时更新界面上所有引用到该变量的地方。</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">incrementCount</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{incrementCount}</span><span class="nt">></span>
Clicked {count} {count === 1 ? 'time' : 'times'}
<span class="nt"></button></span>
</code></pre>
<p>可以使用 <code>$:</code> 来声明一个依赖于另一个变量的响应式变量:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nl">$</span><span class="p">:</span> <span class="nx">doubled</span> <span class="o">=</span> <span class="nx">count</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">handleClick</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">count</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{handleClick}</span><span class="nt">></span>
Clicked {count} {count === 1 ? 'time' : 'times'}
<span class="nt"></button></span>
<span class="nt"><p></span>{count} doubled is {doubled}<span class="nt"></p></span>
</code></pre>
<p>当以上代码中的 <code>count</code> 变化时,<code>doubled</code> 会跟着变。</p>
<p>不仅仅是变量,任意表达式都可以被声明为响应式的:</p>
<pre class="highlight"><code class="language-javascript"><span class="c1">//单行表达式</span>
<span class="nx">$</span><span class="p">:</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">the count is </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">count</span><span class="p">);</span>
<span class="c1">//多行表达式</span>
<span class="nl">$</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">the count is </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">count</span><span class="p">);</span>
<span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">I SAID THE COUNT IS </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">count</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//条件表达式</span>
<span class="nl">$</span><span class="p">:</span> <span class="k">if</span> <span class="p">(</span><span class="nx">count</span> <span class="o">>=</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">count is dangerously high!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">count</span> <span class="o">=</span> <span class="mi">9</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<p>注意,Svelte 的响应性 (reactivity) 是通过赋值操作来触发的。通过数组和对象的方法来改变它们自身的属性不会触发响应性,例如:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">];</span>
<span class="kd">function</span> <span class="nx">addNumber</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">numbers</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">numbers</span><span class="p">.</span><span class="nx">length</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
<span class="nl">$</span><span class="p">:</span> <span class="nx">sum</span> <span class="o">=</span> <span class="nx">numbers</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">t</span><span class="p">,</span> <span class="nx">n</span><span class="p">)</span> <span class="o">=></span> <span class="nx">t</span> <span class="o">+</span> <span class="nx">n</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="nt"></script></span>
<span class="nt"><p></span>{numbers.join(' + ')} = {sum}<span class="nt"></p></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{addNumber}</span><span class="nt">></span> Add a number <span class="nt"></button></span>
</code></pre>
<p>上面的代码通过 <code>Array#push()</code> 来操作数组的内容,因此界面不会跟着变化。</p>
<p>修复这个问题的一个常见方法,就是将该数组重新赋值给自身:</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">function</span> <span class="nx">addNumber</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">numbers</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">numbers</span><span class="p">.</span><span class="nx">length</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="nx">numbers</span> <span class="o">=</span> <span class="nx">numbers</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<p>或者可以用 ES6 的展开语法 (spread syntax):</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">function</span> <span class="nx">addNumber</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">numbers</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">numbers</span><span class="p">,</span> <span class="nx">numbers</span><span class="p">.</span><span class="nx">length</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
<span class="p">}</span>
</code></pre>
<p>数组的其它方法,包括 <code>pop()</code>, <code>shift()</code>, <code>splice()</code> 等,以及对象的方法,如 <code>Map#set()</code>, <code>Set#add()</code> 等,都同样无法触发响应性。</p>
<p>但是对数组、对象自身属性的赋值,又可以触发响应性。例如:</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">let</span> <span class="nx">obj</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">yu</span><span class="dl">'</span><span class="p">};</span>
<span class="kd">function</span> <span class="nx">changeName</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">obj</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">yuan</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<p>又但是,将响应性的变量赋值给另一个变量时,通过第 2 个变量来改变其属性,将不会触发响应性。</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">let</span> <span class="nx">obj</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">yu</span><span class="dl">'</span><span class="p">};</span>
<span class="kd">function</span> <span class="nx">changeName</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">;</span>
<span class="nx">foo</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">yuan</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<p>总之一句话:想要触发响应性,被更新的变量必须直接出现在赋值表达式的左边。</p>
<h2>
<a id="%E5%B1%9E%E6%80%A7+%28Props%29" href="#%E5%B1%9E%E6%80%A7+%28Props%29" class="anchor"></a>属性 (Props)</h2>
<p>嵌套在另一个组件内部的组件,需要从外部组件获取数据时,可以使用 props(properties 的简写)。具体的方法是:在内部组件里 export 一个变量用于接收数据,再在外部组件使用内部组件的标签上加入同名属性。</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Nested</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Nested.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><Nested</span> <span class="na">answer=</span><span class="s">{42}/</span><span class="nt">></span>
<span class="c"><!-- 文件 Nested.svelte --></span>
<span class="nt"><script></span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">answer</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><p></span>The answer is {answer}<span class="nt"></p></span>
</code></pre>
<p>Props 可以设置默认值:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Nested</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Nested.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><Nested</span> <span class="na">answer=</span><span class="s">{42}/</span><span class="nt">></span>
<span class="nt"><Nested</span> <span class="nt">/></span>
<span class="c"><!-- 文件 Nested.svelte --></span>
<span class="nt"><script></span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">answer</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">a mystery</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><p></span>The answer is {answer}<span class="nt"></p></span>
</code></pre>
<p>如果有一个对象,它的每个字段对应着组件里的每一个属性,可以直接将这个对象用解构赋值的方式传递给组件:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Info</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Info.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">pkg</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">,</span>
<span class="na">version</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="na">speed</span><span class="p">:</span> <span class="dl">'</span><span class="s1">blazing</span><span class="dl">'</span><span class="p">,</span>
<span class="na">website</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://svelte.dev</span><span class="dl">'</span>
<span class="p">};</span>
<span class="nt"></script></span>
<span class="nt"><Info</span> <span class="err">{...</span><span class="na">pkg</span><span class="err">}</span><span class="nt">/></span>
<span class="c"><!-- Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/ --></span>
</code></pre>
<p>另外,可以在嵌套组件内部使用 <code>$$props</code> 来访问传入的所有属性。这种方式在即使没有 export 变量的情况下仍然可以访问传进来的属性。不过这是 Svelte 不建议使用的方法,因为 Svelte 无法对其进行优化。</p>
<h2>
<a id="%E9%80%BB%E8%BE%91" href="#%E9%80%BB%E8%BE%91" class="anchor"></a>逻辑</h2>
<h3>
<a id="if+%E6%9D%A1%E4%BB%B6%E7%9A%84%E5%86%99%E6%B3%95" href="#if+%E6%9D%A1%E4%BB%B6%E7%9A%84%E5%86%99%E6%B3%95" class="anchor"></a>if 条件的写法</h3>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">x</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>
<span class="nt"></script></span>
{#if x > 10}
<span class="nt"><p></span>{x} is greater than 10<span class="nt"></p></span>
{:else if 5 > x}
<span class="nt"><p></span>{x} is less than 5<span class="nt"></p></span>
{:else}
<span class="nt"><p></span>{x} is between 5 and 10<span class="nt"></p></span>
{/if}
</code></pre>
<h3>
<a id="each+%E9%81%8D%E5%8E%86" href="#each+%E9%81%8D%E5%8E%86" class="anchor"></a>each 遍历</h3>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">cats</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">J---aiyznGQ</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Keyboard Cat</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">z_AbfPXTKms</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Maru</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">OUtn3pvWmpg</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Henri The Existential Cat</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">];</span>
<span class="nt"></script></span>
<span class="nt"><h1></span>The Famous Cats of YouTube<span class="nt"></h1></span>
<span class="nt"><ul></span>
{#each cats as cat, i}
<span class="nt"><li><a</span> <span class="na">target=</span><span class="s">"_blank"</span> <span class="na">href=</span><span class="s">"https://www.youtube.com/watch?v={cat.id}"</span><span class="nt">></span>
{i + 1}: {cat.name}
<span class="nt"></a></li></span>
{/each}
<span class="nt"></ul></span>
</code></pre>
<h3>
<a id="keyed+each+block" href="#keyed+each+block" class="anchor"></a>keyed each block</h3>
<p>试运行下面的代码:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Thing</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Thing.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">things</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">apple</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">banana</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">carrot</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">doughnut</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">egg</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">];</span>
<span class="kd">function</span> <span class="nx">handleClick</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">things</span> <span class="o">=</span> <span class="nx">things</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{handleClick}</span><span class="nt">></span>Remove first thing<span class="nt"></button></span>
{#each things as thing}
<span class="nt"><Thing</span> <span class="na">name=</span><span class="s">{thing.name}/</span><span class="nt">></span>
{/each}
<span class="c"><!-- 文件 Thing.svelte --></span>
<span class="nt"><script></span>
<span class="kd">const</span> <span class="nx">emojis</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">apple</span><span class="p">:</span> <span class="dl">"</span><span class="s2">🍎</span><span class="dl">"</span><span class="p">,</span>
<span class="na">banana</span><span class="p">:</span> <span class="dl">"</span><span class="s2">🍌</span><span class="dl">"</span><span class="p">,</span>
<span class="na">carrot</span><span class="p">:</span> <span class="dl">"</span><span class="s2">🥕</span><span class="dl">"</span><span class="p">,</span>
<span class="na">doughnut</span><span class="p">:</span> <span class="dl">"</span><span class="s2">🍩</span><span class="dl">"</span><span class="p">,</span>
<span class="na">egg</span><span class="p">:</span> <span class="dl">"</span><span class="s2">🥚</span><span class="dl">"</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">name</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">emoji</span> <span class="o">=</span> <span class="nx">emojis</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
<span class="nt"></script></span>
<span class="nt"><p><span></span>The emoji for { name } is { emoji }<span class="nt"></span></p></span>
<span class="nt"><style></span>
<span class="nt">p</span> <span class="p">{</span> <span class="nl">margin</span><span class="p">:</span> <span class="m">0.8em</span> <span class="m">0</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">span</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0.2em</span> <span class="m">1em</span> <span class="m">0.3em</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.2em</span><span class="p">;</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="m">#FFDFD3</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<p>点击一次按钮,会发现 apple 那一行文字没有了,但是苹果的 emoji 仍然存在,而最后一个鸡蛋的 emoji 却消失了。这是因为 App.svelte 里的 <code>things</code> 在删除第一个元素之后,Svelte 认为第 1 到第 4 个元素发生了变化,就跟着修改了前面 4 个 <code><Thing></code> 的内容,但 Thing.svelte 里的 emoji 变量没有跟着变化,仍然保留着之前的值,所以看到的效果就是 banana 对应的 emoji 变成了一个苹果。同时因为最后一个 <code><Thing></code> 标签没有对应的 <code>things</code> 元素,就被删除了。</p>
<p>当然在 Thing.svelte 里可以使用 <code>$: emoji = emojis[name]</code> 来使 emoji 跟着 name 而变化,但这个例子要说明的问题在于:<code>things</code> 被删除的是第一个元素,而 <code><Thing></code> 被删除的是最后一个元素。所以在本例中,正确的解法是为每个 each block 创建一个键,告诉 Svelte 根据什么来绑定数组和 DOM 元素。</p>
<pre class="highlight"><code class="language-html">{#each things as thing (thing.id)}
<span class="nt"><Thing</span> <span class="na">name=</span><span class="s">{thing.name}/</span><span class="nt">></span>
{/each}
</code></pre>
<h3>
<a id="await+blocks" href="#await+blocks" class="anchor"></a>await blocks</h3>
<p>Svelte 的组件还可以直接在模板上根据一个 Promise 的状态进行渲染不同的内容:</p>
<pre class="highlight"><code class="language-html">{#await promise}
<span class="nt"><p></span>...waiting<span class="nt"></p></span>
{:then number}
<span class="nt"><p></span>The number is {number}<span class="nt"></p></span>
{:catch error}
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"color: red"</span><span class="nt">></span>{error.message}<span class="nt"></p></span>
{/await}
</code></pre>
<p>如果没有必要处理 Promise 的异常,可以把 <code>catch</code> 块省略掉。同时如果你不打算在 Promise 执行完之前显示任何内容,也可以把第一个块省略掉:</p>
<pre class="highlight"><code class="language-html">{#await promise then number}
<span class="nt"><p></span>the number is {number}<span class="nt"></p></span>
{/await}
</code></pre>
<h2>
<a id="%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86" href="#%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86" class="anchor"></a>事件处理</h2>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">m</span> <span class="o">=</span> <span class="p">{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mi">0</span> <span class="p">};</span>
<span class="kd">function</span> <span class="nx">handleMousemove</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">m</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">clientX</span><span class="p">;</span>
<span class="nx">m</span><span class="p">.</span><span class="nx">y</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">clientY</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><div</span> <span class="na">on:mousemove=</span><span class="s">{handleMousemove}</span><span class="nt">></span>
The mouse position is {m.x} x {m.y}
<span class="nt"></div></span>
<span class="nt"><style></span>
<span class="nt">div</span> <span class="p">{</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<p>可以把事件处理器内联 (inline event handler):</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">m</span> <span class="o">=</span> <span class="p">{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mi">0</span> <span class="p">};</span>
<span class="nt"></script></span>
<span class="nt"><div</span> <span class="na">on:mousemove=</span><span class="s">"{e => m = { x: e.clientX, y: e.clientY }}"</span><span class="nt">></span>
The mouse position is {m.x} x {m.y}
<span class="nt"></div></span>
<span class="nt"><style></span>
<span class="nt">div</span> <span class="p">{</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<p>在其它框架中,可能会看到“不要使用内联事件处理“的建议,因为那会引起性能上的问题。但这个问题在 Svelte 中不存在。</p>
<p>另外,在事件处理器的内部,<code>this</code> 指向 DOM 元素本身。</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">function</span> <span class="nx">handleInput</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><input</span> <span class="na">on:input=</span><span class="s">{handleInput}/</span><span class="nt">></span>
</code></pre>
<h3>
<a id="%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6+%28Event+modifiers%29" href="#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6+%28Event+modifiers%29" class="anchor"></a>事件修饰符 (Event modifiers)</h3>
<p>在 Svelte 中可以用事件修饰符来修饰事件处理器,比如 <code>once</code> 修饰符可以让某个事件只被触发一次:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">function</span> <span class="nx">handleClick</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">no more alerts</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><button</span> <span class="na">on:click</span><span class="err">|</span><span class="na">once=</span><span class="s">{handleClick}</span><span class="nt">></span>
Click me
<span class="nt"></button></span>
</code></pre>
<p>可用的修饰符有:</p>
<ul>
<li>preventDefault: 调用 event.preventDefault() 阻止控件触发事件的默认处理行为;</li>
<li>stopPropagation: 调用 event.stopPropagation() 防止事件冒泡和事件捕获;</li>
<li>passive: 提高了触摸/滚轮事件的滚动性能(Svelte 会在安全的地方自动添加它);</li>
<li>nonpassive: 显式地设置 passive: false;</li>
<li>capture: 在捕获阶段触发事件处理,而非冒泡阶段;</li>
<li>once: 执行过一次之后移除该事件处理器;</li>
<li>self: 仅在 event.target 是自身时触发事件处理;</li>
<li>trusted: 仅在 event.isTrusted 为 true 时(例如用户主动触发时)触发事件处理</li>
</ul>
<p>这些修饰符可以同时串联起来使用:<code>on:click|once|capture={...}</code></p>
<h3>
<a id="%E7%BB%84%E4%BB%B6%E4%BA%8B%E4%BB%B6+%28Component+events%29" href="#%E7%BB%84%E4%BB%B6%E4%BA%8B%E4%BB%B6+%28Component+events%29" class="anchor"></a>组件事件 (Component events)</h3>
<p>一个组件自身可以处理许多事件,例如鼠标的移动、点击、文字的输入等。如果这些事件的处理逻辑由组件自身实现,那么将代码写在组件内部即可。如果部分事件的处理逻辑需要外部来实现,那么就需要为组件编写自定义的事件,外部其它组件在使用该组件时,通过 <code>on:自定义事件</code> 的指令 (directive) 来处理事件。</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">handleMessage</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">text</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">on:message=</span><span class="s">{handleMessage}/</span><span class="nt">></span>
<span class="c"><!-- 文件 Inner.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createEventDispatcher</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nx">createEventDispatcher</span><span class="p">();</span>
<span class="kd">function</span> <span class="nx">sayHello</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">dispatch</span><span class="p">(</span><span class="dl">'</span><span class="s1">message</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">text</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Hello!</span><span class="dl">'</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{sayHello}</span><span class="nt">></span>
Click to say hello
<span class="nt"></button></span>
</code></pre>
<p>注意这里的 <code>createEventDispatcher()</code> 必须在组件初次实例化时就被调用,不能放在诸如 <code>setTimeout()</code> 之类的回调函数里。</p>
<h3>
<a id="%E4%BA%8B%E4%BB%B6%E8%BD%AC%E5%8F%91" href="#%E4%BA%8B%E4%BB%B6%E8%BD%AC%E5%8F%91" class="anchor"></a>事件转发</h3>
<p>与 DOM 事件不同,组件的自定义事件没有冒泡机制,因此当一个组件想要监听深度嵌套在其内部的组件时,需要中间的组件为事件作转发。</p>
<p>事件转发的实现很简单,只要在被转发的组件上添加不带值的同名指令即可:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Outer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Outer.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">handleMessage</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">text</span><span class="p">);</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><Outer</span> <span class="na">on:message=</span><span class="s">{handleMessage}/</span><span class="nt">></span>
<span class="c"><!-- 文件 Inner.svelte 同上一个例子,保持不变 --></span>
<span class="c"><!-- 文件 Outer.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Inner</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Inner.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><Inner</span> <span class="na">on:message</span><span class="nt">/></span>
</code></pre>
<p>对于 DOM 事件的转发,也可以用相同的方法。</p>
<h2>
<a id="%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A" href="#%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A" class="anchor"></a>数据绑定</h2>
<p>在组件内部的一个文本框里,可以通过 <code>on:input</code> 事件来实现数据双向绑定:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">woda</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><input</span> <span class="na">value=</span><span class="s">{name}</span> <span class="na">on:input=</span><span class="s">{(e)</span> <span class="err">=</span><span class="nt">></span> name = e.target.value}>
<span class="nt"><h1></span>Hello {name}!<span class="nt"></h1></span>
</code></pre>
<p>但有个更简单的方法:将 <code><input></code> 的 <code>value</code> 属性改为 <code>bind:value</code> 指令:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">woda</span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><input</span> <span class="na">bind:value=</span><span class="s">{name}/</span><span class="nt">></span>
<span class="nt"><h1></span>Hello {name}!<span class="nt"></h1></span>
</code></pre>
<p>如果变量名与属性名相同的话,还可以简写为:<code><input bind:value/></code>.</p>
<h3>
<a id="Group+inputs" href="#Group+inputs" class="anchor"></a>Group inputs</h3>
<p>遇到 radio button 和 checkbox 可以使用 <code>bind:group</code> 指令。前者使用一个简单的变量来绑定,后者需要跟一个数组作绑定。</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">scoops</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">flavours</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Mint choc chip</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">let</span> <span class="nx">menu</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">Cookies and cream</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Mint choc chip</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Raspberry ripple</span><span class="dl">'</span>
<span class="p">];</span>
<span class="kd">function</span> <span class="nx">join</span><span class="p">(</span><span class="nx">flavours</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">flavours</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="nx">flavours</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">flavours</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">, </span><span class="dl">'</span><span class="p">)}</span><span class="s2"> and </span><span class="p">${</span><span class="nx">flavours</span><span class="p">[</span><span class="nx">flavours</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><h2></span>Size<span class="nt"></h2></span>
<span class="nt"><label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">radio</span> <span class="na">bind:group=</span><span class="s">{scoops}</span> <span class="na">name=</span><span class="s">"scoops"</span> <span class="na">value=</span><span class="s">{1}</span><span class="nt">></span>
One scoop
<span class="nt"></label></span>
<span class="nt"><label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">radio</span> <span class="na">bind:group=</span><span class="s">{scoops}</span> <span class="na">name=</span><span class="s">"scoops"</span> <span class="na">value=</span><span class="s">{2}</span><span class="nt">></span>
Two scoops
<span class="nt"></label></span>
<span class="nt"><label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">radio</span> <span class="na">bind:group=</span><span class="s">{scoops}</span> <span class="na">name=</span><span class="s">"scoops"</span> <span class="na">value=</span><span class="s">{3}</span><span class="nt">></span>
Three scoops
<span class="nt"></label></span>
<span class="nt"><h2></span>Flavours<span class="nt"></h2></span>
{#each menu as flavour}
<span class="nt"><label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">checkbox</span> <span class="na">bind:group=</span><span class="s">{flavours}</span> <span class="na">name=</span><span class="s">"flavours"</span> <span class="na">value=</span><span class="s">{flavour}</span><span class="nt">></span>
{flavour}
<span class="nt"></label></span>
{/each}
{#if flavours.length === 0}
<span class="nt"><p></span>Please select at least one flavour<span class="nt"></p></span>
{:else if flavours.length > scoops}
<span class="nt"><p></span>Can't order more flavours than scoops!<span class="nt"></p></span>
{:else}
<span class="nt"><p></span>
You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
of {join(flavours)}
<span class="nt"></p></span>
{/if}
</code></pre>
<p>上面的 checkbox 也可以用 <code><select multiple></code> 替代。</p>
<h3>
<a id="Contenteditable+bindings" href="#Contenteditable+bindings" class="anchor"></a>Contenteditable bindings</h3>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">html</span> <span class="o">=</span> <span class="dl">'</span><span class="s1"><p>Write some text!</p></span><span class="dl">'</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><div</span> <span class="na">contenteditable=</span><span class="s">"true"</span> <span class="na">bind:innerHTML=</span><span class="s">{html}</span><span class="nt">></div></span>
<span class="nt"><pre></span>{html}<span class="nt"></pre></span>
<span class="nt"><style></span>
<span class="o">[</span><span class="nt">contenteditable</span><span class="o">]</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0.5em</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#eee</span><span class="p">;</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
</code></pre>
<h3>
<a id="This" href="#This" class="anchor"></a>This</h3>
<p>每个 DOM 元素和组件都可以通过 <code>bind:this</code> 把自身绑定到一个变量上。但由于界面渲染完成之前,被绑定的DOM 元素或组件还不存在,所以目标变量的值会是 <code>undefined</code>. 因此操作该变量的代码应该放到组件的 <code>onMount()</code> 回调函数内。</p>
<pre class="highlight"><code class="language-html"><span class="nt"><canvas</span>
<span class="na">bind:this=</span><span class="s">{canvas}</span>
<span class="na">width=</span><span class="s">{32}</span>
<span class="na">height=</span><span class="s">{32}</span>
<span class="nt">></canvas></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">onMount</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">canvas</span><span class="p">;</span>
<span class="nx">onMount</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">//...</span>
<span class="p">}</span>
<span class="nt"></script></span>
</code></pre>
<h3>
<a id="%E7%BB%84%E4%BB%B6%E7%BB%91%E5%AE%9A" href="#%E7%BB%84%E4%BB%B6%E7%BB%91%E5%AE%9A" class="anchor"></a>组件绑定</h3>
<p>正如变量可以绑定到 DOM 元素上一样,变量也可以绑定到另一个组件上。</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">Nested</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Nested.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">input</span>
<span class="nt"></script></span>
<span class="nt"><input</span> <span class="na">bind:value=</span><span class="s">{input}/</span><span class="nt">></span>
<span class="nt"><Nested</span> <span class="na">bind:answer=</span><span class="s">{input}/</span><span class="nt">></span>
<span class="c"><!-- 文件 Nested.svelte --></span>
<span class="nt"><script></span>
<span class="k">export</span> <span class="kd">let</span> <span class="nx">answer</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><p></span>The answer is {answer}<span class="nt"></p></span>
</code></pre>
<p>如果组件绑定用得太多,会造成数据流很难跟踪,所以请有节制地使用组件绑定。</p>
<h3>
<a id="%E7%BB%91%E5%AE%9A%E7%BB%84%E4%BB%B6%E5%AE%9E%E4%BE%8B" href="#%E7%BB%91%E5%AE%9A%E7%BB%84%E4%BB%B6%E5%AE%9E%E4%BE%8B" class="anchor"></a>绑定组件实例</h3>
<p><code>bind:this</code> 同样可以用在组件上:</p>
<pre class="highlight"><code class="language-html"><span class="c"><!-- 文件 App.svelte --></span>
<span class="nt"><script></span>
<span class="k">import</span> <span class="nx">InputField</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./InputField.svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">field</span><span class="p">;</span>
<span class="nt"></script></span>
<span class="nt"><InputField</span> <span class="na">bind:this=</span><span class="s">{field}/</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">on:click=</span><span class="s">{()</span> <span class="err">=</span><span class="nt">></span> field.focus()}>Focus field<span class="nt"></button></span>
<span class="c"><!-- 文件 InputField.svelte --></span>
<span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">input</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">focus</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">input</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><input</span> <span class="na">bind:this=</span><span class="s">{input}</span> <span class="nt">/></span>
</code></pre>
<h2>
<a id="%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" href="#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="anchor"></a>生命周期</h2>
<p><code>onMount()</code> 生命周期回调函数会在组件初次渲染之后执行,如果 <code>onMount()</code> 返回了一个函数,该函数会在组件销毁时执行。</p>
<p>为兼容服务器端渲染,最好将 <code>fetch()</code> 请求放在 <code>onMount()</code> 里执行,而不是直接写在 <code><script></code> 里边。除了 <code>onDestroy()</code> 回调函数之外,其它的回调函数都不会在服务端渲染期间执行,因此 <code>fetch()</code> 的执行被延迟到了客户端 DOM 渲染之后。</p>
<p><code>onDestroy()</code> 在组件销毁时执行,常用于执行一些清理操作,例如 <code>setInterval()</code> 的清理。防止内存泄露。</p>
<pre class="highlight"><code class="language-javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">onDestroy</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">interval</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">counter</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1000</span><span class="p">);</span>
<span class="nx">onDestroy</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">clearInterval</span><span class="p">(</span><span class="nx">interval</span><span class="p">));</span>
</code></pre>
<p>为了防止忘记清理,可以自定义一个函数:</p>
<pre class="highlight"><code class="language-javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">onDestroy</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">onInterval</span><span class="p">(</span><span class="nx">callback</span><span class="p">,</span> <span class="nx">milliseconds</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">interval</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(</span><span class="nx">callback</span><span class="p">,</span> <span class="nx">milliseconds</span><span class="p">);</span>
<span class="nx">onDestroy</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">clearInterval</span><span class="p">(</span><span class="nx">interval</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// 使用</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">onInterval</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./utils.js</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nx">onInterval</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">counter</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1000</span><span class="p">);</span>
</code></pre>
<p><code>beforeUpdate()</code> 和 <code>afterUpdate()</code> 分别在组件的 DOM 更新之前和更新之后执行。</p>
<p><code>tick()</code> 方法可以在任何时候调用。它会返回一个 Promise, 并且该 Promise 会在 DOM 的 pending 状态变化之后执行。用一个例子来说明:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><script></span>
<span class="kd">let</span> <span class="nx">text</span> <span class="o">=</span> <span class="s2">`Select some text and hit the tab key to toggle uppercase`</span><span class="p">;</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">handleKeydown</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">key</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">Tab</span><span class="dl">'</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">selectionStart</span><span class="p">,</span> <span class="nx">selectionEnd</span><span class="p">,</span> <span class="nx">value</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">selection</span> <span class="o">=</span> <span class="nx">value</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">selectionStart</span><span class="p">,</span> <span class="nx">selectionEnd</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">replacement</span> <span class="o">=</span> <span class="sr">/</span><span class="se">[</span><span class="sr">a-z</span><span class="se">]</span><span class="sr">/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">selection</span><span class="p">)</span>
<span class="p">?</span> <span class="nx">selection</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span>
<span class="p">:</span> <span class="nx">selection</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="nx">text</span> <span class="o">=</span> <span class="p">(</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">selectionStart</span><span class="p">)</span> <span class="o">+</span>
<span class="nx">replacement</span> <span class="o">+</span>
<span class="nx">value</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">selectionEnd</span><span class="p">)</span>
<span class="p">);</span>
<span class="c1">// 下面的代码没有效果,因为 DOM 还没有更新</span>
<span class="k">this</span><span class="p">.</span><span class="nx">selectionStart</span> <span class="o">=</span> <span class="nx">selectionStart</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">selectionEnd</span> <span class="o">=</span> <span class="nx">selectionEnd</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></script></span>
<span class="nt"><style></span>
<span class="nt">textarea</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">200px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"><textarea</span> <span class="na">value=</span><span class="s">{text}</span> <span class="na">on:keydown=</span><span class="s">{handleKeydown}</span><span class="nt">></textarea></span>
</code></pre>
<p>运行代码之后,选中文本框中一段字符串,按键盘上的 Tab 键,可以看到选中的文本大小写被转换了。但是转换之后,光标移到了文本的末尾,并没有按照代码的意图继续保持刚刚选中的状态。这是因为执行最后两句代码的时候,DOM 还没有更新。而当 DOM 更新之后,最后两句代码已经执行完了。所以这里需要在执行最后两句代码之前,等待 DOM 更新完成,这时候就需要用到 <code>tick()</code>.</p>
<pre class="highlight"><code class="language-javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">tick</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">//...其它代码...</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">handleKeydown</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//...其它代码...</span>
<span class="nx">tick</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">selectionStart</span> <span class="o">=</span> <span class="nx">selectionStart</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">selectionEnd</span> <span class="o">=</span> <span class="nx">selectionEnd</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
Svelte 官方在线编辑和运行代码的工具:https://svelte.dev/repl/hello-world?version=3.49.0
组件 (Component)
在 Svelte...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1422
2022-08-01T07:12:19Z
2022-10-28T15:02:02Z
Kotlin 1.5 协程笔记
<pre class="highlight"><code class="language-kotlin"><span class="c1">// in CoroutineScope</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">users</span> <span class="p">=</span> <span class="nf">loadContributorsSuspend</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="c1">// suspend 方法</span>
<span class="nf">updateResults</span><span class="p">(</span><span class="n">users</span><span class="p">,</span> <span class="n">startTime</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadContributorsSuspend</span><span class="p">(</span><span class="n">service</span><span class="p">:</span> <span class="nc">GitHubService</span><span class="p">,</span> <span class="n">req</span><span class="p">:</span> <span class="nc">RequestData</span><span class="p">):</span> <span class="nc">List</span><span class="p"><</span><span class="nc">User</span><span class="p">></span> <span class="p">{</span>
<span class="o">......</span>
<span class="p">}</span>
</code></pre>
<p>这里的 <code>launch</code> 方法是 <code>CoroutineScope</code> 的一个方法,它能够创建一个协程,Kotlin 里协程被视作“轻量化”的线程,它是可以挂起 (suspend) 的。<code>suspend</code> 修饰的方法叫作 suspend 方法,例如这里的 <code>loadContributorsSuspend()</code>。suspend 方法只能在协程中被调用,或者被另一个 suspend 方法调用。</p>
<p>在一个协程内调用 suspend 方法(例如调用一个发起网络请求的 suspend 方法),该协程会被挂起,变为 suspended 状态,并被从当前线程中移除,保存在内存当中,当前线程可以继续执行其它任务。当该协程准备好继续执行时(例如网络请求完成并返回),协程会重新加入一个线程(不一定是之前的那个线程),继续执行内部剩余部分的代码。</p>
<p><code>runBlocking</code>, <code>launch</code> 和 <code>async</code> 都能用于创建一个协程,区别在于:</p>
<ul>
<li>
<p><code>runBlocking</code> 主要是用于桥接起普通方法和 suspend 方法,桥接起 blocking world 和 non-blocking world. 通常被用作创建最顶层的协程。它的调用会引起当前线程阻塞;</p>
</li>
<li>
<p><code>async()</code> 和 <code>launch()</code> 一样,是 <code>CoroutineScope</code> 的一个方法,它也能够创建一个协程,并且会返回一个 <code>Deferred</code> 对象。<code>Deferred</code> 是一个泛型类,可以对其调用 <code>await()</code> 方法用于获取 <code>async()</code> 所返回的数据。对 <code>await()</code> 方法调用会引起调用该方法的协程(也就是父协程)挂起。对于 <code>Collection<Deferred<T>></code> 类,Kotlin 还提供了一个 <code>awaitAll()</code> 方法,用于获取一组 Deferred 对象的返回结果:</p>
</li>
</ul>
<pre class="highlight"><code class="language-kotlin"><span class="k">import</span> <span class="nn">kotlinx.coroutines.*</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">deferreds</span><span class="p">:</span> <span class="nc">List</span><span class="p"><</span><span class="nc">Deferred</span><span class="p"><</span><span class="nc">Int</span><span class="p">>></span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span>
<span class="nf">async</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span> <span class="p">*</span> <span class="n">it</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Loading $it"</span><span class="p">)</span>
<span class="n">it</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">sum</span> <span class="p">=</span> <span class="n">deferreds</span><span class="p">.</span><span class="nf">awaitAll</span><span class="p">().</span><span class="nf">sum</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"$sum"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>当然,<code>Collection<Job<T>></code> 也有与之对应的 <code>joinAll()</code> 方法。</p>
<ul>
<li>
<p><code>launch()</code> 方法也能够创建一个协程,并且返回一个 <code>Job</code> 对象。<code>Job</code> 是 <code>Deferred</code> 的父类。如果父协程需要等待该协程执行完毕,可以对其调用 <code>join()</code> 方法达到与 <code>Deferred.await()</code> 类似的效果,但区别是 <code>join()</code> 方法不返回值。
当然,因为 <code>Deferred</code> 是 <code>Job</code> 的子类,所以 <code>Deferred</code> 也可以调用 <code>join()</code> 方法,只是这种情况下没有必要使用 <code>Deferred</code>,直接用 <code>Job</code> 即可。</p>
</li>
<li>
<p><code>async()</code> 会将异常包装在 <code>Deferred</code> 对象里返回,<code>launch()</code> 则会抛出未捕获的异常。</p>
</li>
</ul>
<p>要注意的是,<code>launch()</code> 和 <code>async()</code> 是创建协程,并返回一个 <code>Job</code>, 这里的 Job 和协程不是一回事。在 JVM 上,Kotlin 的协程本质是一个被管理着的线程。</p>
<p><code>runBlocking()</code>, <code>launch()</code>, <code>async()</code> 以及后面会提到的 <code>withContext()</code> <code>coroutineScope()</code> 等方法,它们都接收一个 block 参数,该参数被定义为 <code>CoroutineScope</code> 的一个扩展方法,所以在这些 block 内部可以直接调用 <code>CoroutineScope</code> 的实例方法,其中就包括 <code>launch()</code>, <code>async()</code>.</p>
<h2>
<a id="Channel" href="#Channel" class="anchor"></a>Channel</h2>
<p>Kotlin 的 coroutine 包里提供了个叫作 <code>Channel</code> 的类,用于在各协程之间通信。Channel 里可以存放任意类型的数据。提供的操作就是生产者方 send, 消费者方 receive, 很常见的生产-消费模式。</p>
<p>Kotlin 里的 <code>Channel</code> 是个接口,继承了 <code>SendChannel</code> 和 <code>ReceiveChannel</code>,前者有一个 <code>send()</code> 和一个 <code>close()</code> 方法,后者有一个 <code>receive()</code> 方法。<code>send()</code> 方法和 <code>receive()</code> 方法都是 suspend 方法。</p>
<p>这里主要记录一下各种类型 Channel 的异同。</p>
<ul>
<li>
<p>Unlimited Channel: 容量无限,因此 <code>send()</code> 方法不会被挂起,但如果内存不足的话会抛出异常 <code>OutOfMemoryException</code>;当 channel 是空的的时候,<code>receive()</code> 方法会被挂起;</p>
</li>
<li>
<p>Buffered Channel: 设定了容量的 Channel,<code>send()</code> 方法在 channel 满的时候会挂起;</p>
</li>
<li>
<p>Rendezvous Channel: 相当于容量为 0 的 Buffered Channel。当 <code>send()</code> 方法被调用,并且 <code>receive()</code> 方法没有被调用时,<code>send()</code> 方法会被挂起,直到 <code>receive()</code> 方法被调用;同样,当 <code>receive()</code> 方法被调用,并且 <code>send()</code> 方法没有被调用的时候,<code>receive()</code> 方法会被挂起;</p>
</li>
<li>
<p>Conflated Channel: <code>send()</code> 方法连续调用,并且 channel 里的元素没有被消费的话,先前的元素会被后来的元素所覆盖,消费方只能取到最后放进去的元素。</p>
</li>
</ul>
<p>Kotlin 提供了一个工厂方法 <code>Channel()</code> 用来创建不同类型的 Channel。不传任何参数时,默认创建的是 Rendezvous Channel;当参数为任意正整数时,创建的是 Buffered Channel,参数为该 Channel 的容量;当参数为 <code>Channel.UNLIMITED</code>, <code>Channel.CONFLATED</code> 等用于指定类型的常量时,创建的就是对应类型的 Channel。</p>
<h2>
<a id="%E5%BB%B6%E8%BF%9F%E6%89%A7%E8%A1%8C" href="#%E5%BB%B6%E8%BF%9F%E6%89%A7%E8%A1%8C" class="anchor"></a>延迟执行</h2>
<p><code>launch()</code> 和 <code>async()</code> 的 start 参数可以用来指定协程的启动模式。<code>async(start = CoroutineStart.LAZY)</code> 可以让协程延迟执行,直到 <code>start()</code> 方法被调用。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">one</span> <span class="p">=</span> <span class="nf">async</span><span class="p">(</span><span class="n">start</span> <span class="p">=</span> <span class="nc">CoroutineStart</span><span class="p">.</span><span class="nc">LAZY</span><span class="p">)</span> <span class="p">{</span> <span class="nf">doSomethingUsefulOne</span><span class="p">()</span> <span class="p">}</span>
<span class="kd">val</span> <span class="py">two</span> <span class="p">=</span> <span class="nf">async</span><span class="p">(</span><span class="n">start</span> <span class="p">=</span> <span class="nc">CoroutineStart</span><span class="p">.</span><span class="nc">LAZY</span><span class="p">)</span> <span class="p">{</span> <span class="nf">doSomethingUsefulTwo</span><span class="p">()</span> <span class="p">}</span>
<span class="c1">// some computation</span>
<span class="n">one</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span> <span class="c1">// start the first one</span>
<span class="n">two</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span> <span class="c1">// start the second one</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"all coroutine started"</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The answer is ${one.await() + two.await()}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Completed in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">doSomethingUsefulOne</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span> <span class="c1">// pretend we are doing something useful here</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"do something useful one"</span><span class="p">)</span>
<span class="k">return</span> <span class="mi">13</span>
<span class="p">}</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">doSomethingUsefulTwo</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span> <span class="c1">// pretend we are doing something useful here, too</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"do something useful two"</span><span class="p">)</span>
<span class="k">return</span> <span class="mi">29</span>
<span class="p">}</span>
</code></pre>
<p>运行这段代码会发现总运行时间为 1 秒左右。但是如果把代码中的两处 <code>start()</code> 调用删掉,会发现用时为 2 秒左右。因为 <code>await()</code> 会挂起当前协程,使两个协程按 <code>await()</code> 的调用顺序执行。再同时删除掉 <code>async()</code> 的参数的话,运行时间又变回 1 秒左右了。</p>
<h2>
<a id="%E5%8F%96%E6%B6%88%E5%92%8C%E8%B6%85%E6%97%B6" href="#%E5%8F%96%E6%B6%88%E5%92%8C%E8%B6%85%E6%97%B6" class="anchor"></a>取消和超时</h2>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
<span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm sleeping $i ..."</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500L</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span> <span class="c1">// delay a bit</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: I'm tired of waiting!"</span><span class="p">)</span>
<span class="n">job</span><span class="p">.</span><span class="nf">cancel</span><span class="p">()</span> <span class="c1">// cancels the job</span>
<span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span> <span class="c1">// waits for job's completion </span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: Now I can quit."</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>这里的 job 会在执行第 3 次时被取消。<code>join()</code> 方法的调用,是为了确保当前协程退出前,job 里的代码执行完毕(包括资源释放)。<code>cancel()</code> 方法不能在 <code>join()</code> 之后调用,因为先调用 <code>join()</code>,当前协程会等待 job 执行完毕,而 job 内部耗时很长,如果等待其执行完毕,那么后面的 <code>cancel()</code> 就完全没有必要了。而如果 job 内部是个死循环,则 <code>cancel()</code> 方法永远执行不到。</p>
<p>由于 <code>cancel()</code> 和 <code>join()</code> 常常同时使用,Kotlin 另外提供了个 <code>cancelAndJoin()</code> 的快捷方法。</p>
<p><code>kotlinx.coroutines</code> 包里的所有 suspend 方法都是可取消的 (cancellable),这些方法会检查协程的取消动作 (cancellation),并在取消时抛出 <code>CancellationException</code> 异常。但如果一个协程内没有任何 suspend 方法的调用,则无法取消。可以理解为需要被取消的是协程内部的 suspend 方法。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">startTime</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">nextPrintTime</span> <span class="p">=</span> <span class="n">startTime</span>
<span class="kd">var</span> <span class="py">i</span> <span class="p">=</span> <span class="mi">0</span>
<span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="p"><</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// computation loop, just wastes CPU</span>
<span class="c1">// print a message twice a second</span>
<span class="k">if</span> <span class="p">(</span><span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span> <span class="p">>=</span> <span class="n">nextPrintTime</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm sleeping ${i++} ..."</span><span class="p">)</span>
<span class="n">nextPrintTime</span> <span class="p">+=</span> <span class="mi">500L</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span> <span class="c1">// delay a bit</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: I'm tired of waiting!"</span><span class="p">)</span>
<span class="n">job</span><span class="p">.</span><span class="nf">cancelAndJoin</span><span class="p">()</span> <span class="c1">// cancels the job and waits for its completion</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: Now I can quit."</span><span class="p">)</span>
</code></pre>
<p>上面的代码会一直执行到 5 次打印结束。如果你想让这种没有 suspend 方法调用的代码可取消,一种办法是加入一个 suspend 方法,<code>yield()</code> 通常可以起到这个作用。另一个办法是加入 <code>isActive</code> 作为判断条件。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="p"><</span> <span class="mi">5</span> <span class="p">&&</span> <span class="n">isActive</span><span class="p">)</span> <span class="p">{</span>
<span class="o">........</span>
<span class="p">}</span>
</code></pre>
<p>前面提到取消协程会抛出 <code>CancellationException</code> 异常,如果需要在协程取消时释放资源,可以把释放资源的代码写在 <code>finally</code> 块里。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm sleeping $i ..."</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500L</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm running finally"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span> <span class="c1">// delay a bit</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: I'm tired of waiting!"</span><span class="p">)</span>
<span class="n">job</span><span class="p">.</span><span class="nf">cancelAndJoin</span><span class="p">()</span> <span class="c1">// cancels the job and waits for its completion</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: Now I can quit."</span><span class="p">)</span>
</code></pre>
<p>任何企图在 <code>finally</code> 里执行的 suspend 方法,在当被捕获的异常为 <code>CancellationException</code> 时,都会同样抛出 <code>CancellationException</code>,这通常不是什么问题。因为通常来说,释放资源的代码都是非阻塞 (non-blocking) 的。但在某些罕见的情况下,如果你需要在 <code>finally</code> 里调用 suspend 方法,可以使用 <code>withContext(NonCancellable) {...}</code></p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm sleeping $i ..."</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500L</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="nf">withContext</span><span class="p">(</span><span class="nc">NonCancellable</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: I'm running finally"</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"job: And I've just delayed for 1 sec because I'm non-cancellable"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span> <span class="c1">// delay a bit</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: I'm tired of waiting!"</span><span class="p">)</span>
<span class="n">job</span><span class="p">.</span><span class="nf">cancelAndJoin</span><span class="p">()</span> <span class="c1">// cancels the job and waits for its completion</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main: Now I can quit."</span><span class="p">)</span>
</code></pre>
<p>如果想为某 suspend 方法的调用设置超时时间,可以使用 <code>withTimeout()</code> 方法</p>
<pre class="highlight"><code class="language-kotlin"><span class="nf">withTimeout</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"I'm sleeping $i ..."</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500L</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>该方法会在其内部的 suspend 方法执行超时时将其取消,并抛出 <code>TimeoutCancellationException</code>,该异常是 <code>CancellationException</code> 的子类。但这里有个区别是:前者被引发时,会输出异常堆栈信息,后者不会有任何输出。</p>
<p>因为我们会为 <code>CancellationException</code> 在 <code>finally</code> 块里编写资源释放的代码,所以没有必要再专门为 <code>TimeoutCancellationException</code> 写一遍。如果有专门针对该子类异常的处理逻辑,可以写在 <code>try {...} catch (e: TimeoutCancellationException) {...}</code> 里边,或者使用 <code>withTimeoutOrNull()</code> 方法,该方法在超时时不会抛出异常,取而代之的是返回一个 <code>null</code> 值。</p>
<h2>
<a id="%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91+%28Structured+Concurrency%29" href="#%E7%BB%93%E6%9E%84%E5%8C%96%E5%B9%B6%E5%8F%91+%28Structured+Concurrency%29" class="anchor"></a>结构化并发 (Structured Concurrency)</h2>
<p>协程是可以嵌套的,每一个协程自己界定了一个范围 (CoroutineScope),限制了在其范围内的子协程的寿命。父协程被取消,所有子协程都将被取消;父协程的结束,也必须等待所有子协程结束。</p>
<p><code>coroutineScope()</code> 方法可用于在不创建协程的情况下创建一个 <code>CoroutineScope</code>,继承了当前上下文 (<code>CoroutineContext</code>) 中除了 Job 元素以外的所有的元素 (<code>Element</code>). 它是个 suspend 方法,会挂起当前的协程,直至内部协程执行完毕。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">runBlocking</span> <span class="p">{</span>
<span class="nf">coroutineScope</span> <span class="p">{</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"- World 2"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"- World"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>输出</p>
<pre class="highlight"><code>Hello
- World
- World 2
Done
</code></pre>
<p>可以看到打印 Hello 之后没有接着打印 Done,而是等待内部的两个协程执行完。如果去掉这个 coroutingScope 的调用,则会输出:</p>
<pre class="highlight"><code>Hello
Done
- World
- World 2
</code></pre>
<p>Kotlin 另外提供了一个 <code>CoroutineScope()</code> 方法,它与 <code>coroutineScope()</code> 的区别在于,前者是用于创建并返回一个 <code>CoroutineScope</code> 实例,可以用一个变量指向它,在需要的地方调用它的 <code>launch()</code> 等方法来创建协程。后者则是直接在当前上下文当中就地创建一个 <code>CoroutineScope</code> (并没有创建协程)并执行其中的代码。</p>
<p>考虑如下代码</p>
<pre class="highlight"><code class="language-kotlin"><span class="c1">// The result type of somethingUsefulOneAsync is Deferred<Int></span>
<span class="nd">@OptIn</span><span class="p">(</span><span class="nc">DelicateCoroutinesApi</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">somethingUsefulOneAsync</span><span class="p">()</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">async</span> <span class="p">{</span>
<span class="nf">doSomethingUsefulOne</span><span class="p">()</span>
<span class="p">}</span>
<span class="c1">// The result type of somethingUsefulTwoAsync is Deferred<Int></span>
<span class="nd">@OptIn</span><span class="p">(</span><span class="nc">DelicateCoroutinesApi</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">somethingUsefulTwoAsync</span><span class="p">()</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">async</span> <span class="p">{</span>
<span class="nf">doSomethingUsefulTwo</span><span class="p">()</span>
<span class="p">}</span>
<span class="c1">// note that we don't have `runBlocking` to the right of `main` in this example</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="c1">// we can initiate async actions outside of a coroutine</span>
<span class="kd">val</span> <span class="py">one</span> <span class="p">=</span> <span class="nf">somethingUsefulOneAsync</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">two</span> <span class="p">=</span> <span class="nf">somethingUsefulTwoAsync</span><span class="p">()</span>
<span class="c1">// but waiting for a result must involve either suspending or blocking.</span>
<span class="c1">// here we use `runBlocking { ... }` to block the main thread while waiting for the result</span>
<span class="nf">runBlocking</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The answer is ${one.await() + two.await()}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Completed in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>这里在 <code>runBlocking()</code> 之外使用了 <code>GlobalScope.async()</code>,它创建的协程与其它协程没有任何层级关系。假如 <code>two</code> 执行出错,抛出异常,<code>one</code> 会仍然在后台执行。在结构化并发的写法中,这种情况可以避免。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">concurrentSum</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">=</span> <span class="nf">coroutineScope</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">one</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">doSomethingUsefulOne</span><span class="p">()</span> <span class="p">}</span>
<span class="kd">val</span> <span class="py">two</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">doSomethingUsefulTwo</span><span class="p">()</span> <span class="p">}</span>
<span class="n">one</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span> <span class="p">+</span> <span class="n">two</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The answer is ${concurrentSum()}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Completed in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>这里使用 <code>coroutineScope()</code> 创建了一个 <code>CoroutineScope</code>, 在其内部创建的协程算作该 coroutine scope 的子协程,如果 <code>concurrentSum()</code> 内部有任何代码抛出异常,它内部创建的所有协程都会被取消。这也是“结构化并发”的一个特点。结构化并发有助于管理协程的生命周期和资源占用。</p>
<h2>
<a id="Context+%E5%92%8C+Dispatcher" href="#Context+%E5%92%8C+Dispatcher" class="anchor"></a>Context 和 Dispatcher</h2>
<p>Kotlin 的协程总是运行在一个叫 CoroutineContext 的环境里,CoroutineContext 里包含的主要元素 (element) 有协程本身的 <code>Job</code>, <code>CoroutineDispatcher</code>, <code>CoroutineExceptionHandler</code> 和 <code>CoroutineName</code>.</p>
<p><code>CoroutineDispatcher</code> 是实现了 <code>CoroutineContext</code> 的一个抽象类,它可以用来决定协程该运行在哪个线程或哪个线程池里的线程上。</p>
<p><code>launch</code> 和 <code>async</code> 接受一个可选的 CoroutineContext 参数,该参数用于显式地指定协程及其上下文 (Context) 中的其它元素 (element) 所使用的 dispatcher. 不指定参数的时候,新的协程从父级的 CoroutineScope 里继承 context 和 dispatcher。</p>
<p><code>Dispatchers.Default</code> 是 CoroutineDispatcher 的其中一种实现。当你给 <code>async()</code> 或者 <code>launch()</code> 方法传递参数 <code>Dispatchers.Default</code> 时,它会从 JVM 的一个共享线程池里获取空闲的线程。这个共享线程池的线程数量与 CPU 的核心数量相等。但是当 CPU 只有一个核心时,线程池里会有两个线程。只要并发量足够大,便可以在日志中打印线程 ID 来观察到效果。同时我们会看到,启动协程的线程,和协程由 suspended 状态恢复后所在的线程很多时候并不是同一个线程。</p>
<p>实际上 <code>Dispatchers.Default</code> 比较适用于 CPU 计算密集型的任务。另外 Kotlin 还提供了 <code>Dispatchers.IO</code>,从名字上很容易看出,这个 Dispatcher 适用于 IO 密集型的任务。如果希望协程只在主线程当中运行,可以使用 <code>Dispatchers.Main</code>。<code>MainScope()</code> 工厂方法创建的 CoroutineScope 默认使用 <code>Dispatchers.Main</code>.</p>
<p>还有一个 <code>withContext()</code> 方法,它的作用是创建一个协程,然后把父协程挂起,等待代码块执行完毕,并返回结果。其作用可以认为与 <code>async(){}.await()</code> 效果相同。实际上,当你写出 <code>async(CoroutineDispatcher){}.await()</code> 这样的代码时,编译器会提示你把这段代码用 <code>withContext(CoroutineDispatcher)</code> 来替代。</p>
<p>使用 <code>Dispatchers.Unconfined</code> 时,新的协程会先在创建协程的那个线程中执行,直到遇到协程内部的 suspend 方法,之后的代码会在运行 suspend 代码的那个线程上执行。总之就是很随意。适用于不消耗 CPU,又不更新任何共享数据(比如 UI 状态)的操作。(这是官方说法。我暂时也想不到适用于哪里。)</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Unconfined</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// not confined -- will work with main thread</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Unconfined : I'm working in thread ${Thread.currentThread().name}"</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Unconfined : After delay in thread ${Thread.currentThread().name}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span> <span class="c1">// context of the parent, main runBlocking coroutine</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main runBlocking: I'm working in thread ${Thread.currentThread().name}"</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"main runBlocking: After delay in thread ${Thread.currentThread().name}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// Unconfined : I'm working in thread main @coroutine#2</span>
<span class="c1">// main runBlocking: I'm working in thread main @coroutine#3</span>
<span class="c1">// Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor@coroutine#2</span>
<span class="c1">// main runBlocking: After delay in thread main @coroutine#3</span>
</code></pre>
<h2>
<a id="Flow" href="#Flow" class="anchor"></a>Flow</h2>
<p>Flow 的用法有点类似 sequence, 也有个 flow builder.</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span> <span class="c1">// flow builder</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="c1">// pretend we are doing something useful here</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="c1">// emit next value</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>不同的是 flow 得运行在协程里。确切地说,是 flow 的 terminal operator (示例里的 <code>collect()</code>)得运行在协程里。至于什么是 terminal operator 后面会提到。</p>
<h3>
<a id="RestrictsSuspension" href="#RestrictsSuspension" class="anchor"></a>RestrictsSuspension</h3>
<p>另外,sequence 有个特点是:在其内部无法执行自身以外的 suspend 方法。想要理解这一点,需要先搞清楚 RestrictsSuspension 这个注解的作用。<a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-restricts-suspension/">官方文档</a>里写着:</p>
<blockquote>
<p>Classes and interfaces marked with this annotation are restricted when used as receivers for extension suspend functions. These suspend extensions can only invoke other member or extension suspend functions on this particular receiver and are restricted from calling arbitrary suspension functions.</p>
</blockquote>
<p>意思是,被该注解标记的类和接口,它们的扩展 suspend 方法 (extension suspend function) 无法调用定义于其外部的任何其它 suspend 方法。用一段代码来理解:</p>
<pre class="highlight"><code class="language-kotlin"><span class="nd">@RestrictsSuspension</span>
<span class="kd">class</span> <span class="nc">RestrictsSuspensionClass</span> <span class="p">{</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">a</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"RestrictsSuspensionClass fun a"</span><span class="p">)</span>
<span class="nc">NormalClass</span><span class="p">().</span><span class="nf">a</span><span class="p">()</span> <span class="c1">// (1)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1">// (2)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nc">RestrictsSuspensionClass</span><span class="p">.</span><span class="nf">b</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"RestrictsSuspensionClass fun b"</span><span class="p">)</span>
<span class="nc">NormalClass</span><span class="p">().</span><span class="nf">a</span><span class="p">()</span> <span class="c1">// (3)</span>
<span class="k">this</span><span class="p">.</span><span class="nf">a</span><span class="p">()</span> <span class="c1">// (4)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1">// (5)</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">NormalClass</span> <span class="p">{</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">a</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"NormalClass fun a"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>这段代码里,(1) 和 (2) 能通过编译,是因为这两处的调用在 RestrictsSuspensionClass 的 <code>fun a()</code> 内部,这个 <code>fun a()</code> 不是一个扩展方法,它对其它 suspend 方法的调用不受限制。</p>
<p>(3) 无法通过编译,是因为 <code>fun RestrictsSuspensionClass.b()</code> 是一个扩展方法,并且 <code>NormalClass().a()</code> 是一个定义于 NormalClass 之上的 suspend 方法。</p>
<p>(4) 也是一个 suspend 方法,但它是 RestrictsSuspensionClass 自己的方法,所以可以通过编译。</p>
<p>(5) 无法通过编译的原因和 (3) 一样,<code>delay()</code> 是一个定义于 RestrictsSuspensionClass 之外的 suspend 方法。</p>
<p>现在再回头看,<code>sequence</code> 这个 sequence builder 接受的参数是个 lambda,并且该 lambda 被定义为 SequenceScope 的成员方法。而 SequenceScope 本身又被 RestrictsSuspension 这个注解标记,所以无法调用 <code>delay()</code> 方法。如果需要类似 sequence 的特性,而又想在其内部调用任意 suspend 方法,则需要考虑使用 flow.</p>
<p>和 sequence 一样,flow 也是惰性的,在调用 <code>collect()</code> 等 terminal operator 之前,其内部的代码不会被执行。</p>
<h3>
<a id="flow+%E7%9A%84%E5%8F%96%E6%B6%88" href="#flow+%E7%9A%84%E5%8F%96%E6%B6%88" class="anchor"></a>flow 的取消</h3>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Emitting $i"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">withTimeoutOrNull</span><span class="p">(</span><span class="mi">250</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Timeout after 250ms </span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>执行这段代码,会发现它只打印了 1 和 2.</p>
<h3>
<a id="%E5%88%9B%E5%BB%BA+flow+%E7%9A%84%E6%96%B9%E6%B3%95" href="#%E5%88%9B%E5%BB%BA+flow+%E7%9A%84%E6%96%B9%E6%B3%95" class="anchor"></a>创建 flow 的方法</h3>
<p>除了上述示例中用到的 flow builder 以外,还有以下创建 flow 的方式:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">flow1</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span>
<span class="nf">sequenceOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span>
</code></pre>
<h3>
<a id="Transformation+operator" href="#Transformation+operator" class="anchor"></a>Transformation operator</h3>
<p>flow 有许多 transformation operators, 包括在 Iterable 里常见的 <code>map()</code>, <code>filter()</code> 等。还有个更基础的 <code>transform()</code>:</p>
<pre class="highlight"><code class="language-kotlin"><span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span> <span class="c1">// a flow of requests</span>
<span class="p">.</span><span class="nf">transform</span> <span class="p">{</span> <span class="n">request</span> <span class="p">-></span>
<span class="nf">emit</span><span class="p">(</span><span class="s">"Making request $request"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="nf">performRequest</span><span class="p">(</span><span class="n">request</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">response</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="p">}</span>
</code></pre>
<h3>
<a id="Size-limiting+operator" href="#Size-limiting+operator" class="anchor"></a>Size-limiting operator</h3>
<p>flow 还有个 <code>take()</code> 方法叫作 size-limiting operator, 它在取得足够多的结果之后会取消该 flow 的执行,同时像协程的取消操作一样,flow 的取消会抛出一个异常。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">numbers</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">emit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"This line will not execute"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Finally in numbers"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">numbers</span><span class="p">()</span>
<span class="p">.</span><span class="nf">take</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1">// take only the first two</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
<h3>
<a id="Terminal+operator" href="#Terminal+operator" class="anchor"></a>Terminal operator</h3>
<p><code>toList()</code>, <code>toSet()</code>, <code>first()</code>, <code>reduce()</code>, <code>fold()</code> 这些从 flow 里取结果的方法,包括前面提到的 <code>collect()</code>,叫作 terminal operator.</p>
<h3>
<a id="Flow+%E7%9A%84%E4%B8%8A%E4%B8%8B%E6%96%87" href="#Flow+%E7%9A%84%E4%B8%8A%E4%B8%8B%E6%96%87" class="anchor"></a>Flow 的上下文</h3>
<p>Flow 执行的上下文默认为调用该 flow 的 terminal operator 时所在的上下文,这种特性叫作“上下文保留” (context preservation).</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="nf">log</span><span class="p">(</span><span class="s">"Started simple flow"</span><span class="p">)</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">log</span><span class="p">(</span><span class="s">"Collected $value"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="p"><</span><span class="nc">T</span><span class="p">></span> <span class="nf">log</span><span class="p">(</span><span class="n">msg</span><span class="p">:</span> <span class="nc">T</span><span class="p">)</span> <span class="p">=</span> <span class="nf">println</span><span class="p">(</span><span class="s">"[${Thread.currentThread().name}] $msg"</span><span class="p">)</span>
</code></pre>
<p>如果想要改变 flow 的执行上下文,需要使用 <code>flowOn()</code> 方法。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="nf">log</span><span class="p">(</span><span class="s">"Started simple flow"</span><span class="p">)</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}.</span><span class="nf">flowOn</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">log</span><span class="p">(</span><span class="s">"Collected $value"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="p"><</span><span class="nc">T</span><span class="p">></span> <span class="nf">log</span><span class="p">(</span><span class="n">msg</span><span class="p">:</span> <span class="nc">T</span><span class="p">)</span> <span class="p">=</span> <span class="nf">println</span><span class="p">(</span><span class="s">"[${Thread.currentThread().name}] $msg"</span><span class="p">)</span>
</code></pre>
<h3>
<a id="%E7%BC%93%E5%86%B2+%28Buffering%29" href="#%E7%BC%93%E5%86%B2+%28Buffering%29" class="anchor"></a>缓冲 (Buffering)</h3>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="c1">// pretend we are asynchronously waiting 1 second</span>
<span class="nf">log</span><span class="p">(</span><span class="s">"processing before emit"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="c1">// emit next value</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span> <span class="c1">// pretend we are processing it for 2 seconds</span>
<span class="nf">log</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Collected in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>这段代码在执行时,先 delay 1 秒,接着执行 collector (emit 里的代码),耗时 2 秒,再 delay 1 秒,接着执行 collector … 总耗时 9 秒。如果用上 <code>buffer()</code>,会开启另一个协程去执行 collector,然后直接返回,执行下一个 1 秒的 delay。这样由于后两个 1 秒的 delay 花费的时间包含在了更花时间的 emit 里,最终总耗时 7 秒:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">buffer</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span> <span class="c1">// pretend we are processing it for 2 seconds</span>
<span class="nf">log</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Collected in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<h3>
<a id="%E5%90%88%E6%B5%81+%28Conflation%29" href="#%E5%90%88%E6%B5%81+%28Conflation%29" class="anchor"></a>合流 (Conflation)</h3>
<p><code>conflate()</code> 的表现和 <code>buffer()</code> 差不多,区别在于,使用 <code>buffer()</code> 时,如果单个 collector 耗时太长,后续的所有 collector 会一直等待,直到挨个执行完毕。而使用 <code>conflate()</code> 时,单个 collector 如果耗时太长,只会有最新的一个 collector 在等待,后续 emit 的 collector 会覆盖之前的。行为跟前面提到的 conflated channel 一样。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="nf">conflate</span><span class="p">()</span> <span class="c1">// conflate emissions, don't process each one</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span> <span class="c1">// pretend we are processing it for 2 seconds</span>
<span class="nf">log</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">log</span><span class="p">(</span><span class="s">"Collected in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<h3>
<a id="%E5%A4%84%E7%90%86%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E5%80%BC" href="#%E5%A4%84%E7%90%86%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E5%80%BC" class="anchor"></a>处理最后一个值</h3>
<p>Flow 还提供了一系列 xxxLatest 的 operator, 例如:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">time</span> <span class="p">=</span> <span class="nf">measureTimeMillis</span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="nf">collectLatest</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">)</span> <span class="c1">// pretend we are processing it for 2 seconds</span>
<span class="nf">log</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">log</span><span class="p">(</span><span class="s">"Collected in $time ms"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>行为与 <code>conflate()</code> 比较像,区别在于,使用 xxxLatest, 后续 emit 的 collector 不是覆盖之前的,而是将当前的 collector 取消,直接开始执行新 emit 的 collector. 当然如果 collector 执行得足够快,在下一个 collector 被 emit 之前执行完,就不会被取消。</p>
<p>同样类似的 operator 还有 <code>flatMapLatest()</code>, <code>mapLatest()</code> 和 <code>transformLatest()</code>.</p>
<h3>
<a id="Zip" href="#Zip" class="anchor"></a>Zip</h3>
<p><code>zip()</code> 可以把两个 flow 用你指定的方式合并成成一个 flow:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">nums</span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span> <span class="c1">// numbers 1..3</span>
<span class="kd">val</span> <span class="py">strs</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="s">"one"</span><span class="p">,</span> <span class="s">"two"</span><span class="p">,</span> <span class="s">"three"</span><span class="p">)</span> <span class="c1">// strings </span>
<span class="n">nums</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">strs</span><span class="p">)</span> <span class="p">{</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="p">-></span> <span class="s">"$a -> $b"</span> <span class="p">}</span> <span class="c1">// compose a single string</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// collect and print</span>
</code></pre>
<p>Sequence 也支持同样的操作</p>
<h3>
<a id="Combine" href="#Combine" class="anchor"></a>Combine</h3>
<p><code>combine()</code> 的行为和 <code>zip()</code> 差不多,区别在于如果两个 flow 的执行时间不一样,<code>zip()</code> 会按顺序将两个 flow 的执行结果一一对应起来,而 <code>combine()</code> 则是无论哪个 flow 先执行完,都会去另一个 flow 里取当前值。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">nums</span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">onEach</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="mi">300</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// numbers 1..3 every 300 ms</span>
<span class="kd">val</span> <span class="py">strs</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="s">"one"</span><span class="p">,</span> <span class="s">"two"</span><span class="p">,</span> <span class="s">"three"</span><span class="p">).</span><span class="nf">onEach</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="mi">400</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// strings every 400 ms</span>
<span class="kd">val</span> <span class="py">startTime</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span> <span class="c1">// remember the start time </span>
<span class="n">nums</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">strs</span><span class="p">)</span> <span class="p">{</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="p">-></span> <span class="s">"$a -> $b"</span> <span class="p">}</span> <span class="c1">// compose a single string with "zip"</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="c1">// collect and print </span>
<span class="nf">println</span><span class="p">(</span><span class="s">"$value at ${System.currentTimeMillis() - startTime} ms from start"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>可以将以上代码里的 <code>zip()</code> 改成 <code>combine()</code> 对比输出结果。</p>
<h3>
<a id="Flow+%E5%B1%95%E5%BC%80" href="#Flow+%E5%B1%95%E5%BC%80" class="anchor"></a>Flow 展开</h3>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">requestFlow</span><span class="p">(</span><span class="n">i</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">String</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="nf">emit</span><span class="p">(</span><span class="s">"$i: First"</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span> <span class="c1">// wait 500 ms</span>
<span class="nf">emit</span><span class="p">(</span><span class="s">"$i: Second"</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">startTime</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span> <span class="c1">// remember the start time</span>
<span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">onEach</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="mi">300</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// a number every 300 ms</span>
<span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="nf">requestFlow</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="c1">// collect and print</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"$value at ${System.currentTimeMillis() - startTime} ms from start"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>以上代码的 collector 里,value 本身是个 flow,因为 <code>requestFlow()</code> 的返回值是个 flow. <code>map()</code> 操作之后的结果实际上是个 Flow<Flow>. 要拿到里面的字符串,就需要对其展开。这时候需要用 <code>flatMapConcat()</code> 方法来替换 <code>map()</code>.</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">startTime</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">currentTimeMillis</span><span class="p">()</span> <span class="c1">// remember the start time</span>
<span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">onEach</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="mi">300</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// a number every 300 ms</span>
<span class="p">.</span><span class="nf">flatMapConcat</span> <span class="p">{</span> <span class="nf">requestFlow</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="c1">// collect and print</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"$value at ${System.currentTimeMillis() - startTime} ms from start"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>这样就能得到期望的结果。<code>flatMapMerge()</code> 则可以让 <code>flatMapConcat()</code> 的操作并发执行。<code>flatMapLatest()</code> 则类似 <code>collectLatest()</code>.</p>
<h3>
<a id="%E5%BC%82%E5%B8%B8%E7%9A%84%E9%80%8F%E6%98%8E%E6%80%A7" href="#%E5%BC%82%E5%B8%B8%E7%9A%84%E9%80%8F%E6%98%8E%E6%80%A7" class="anchor"></a>异常的透明性</h3>
<p>Kotlin 不建议在 flow builder 内部的 <code>try/catch</code> 块内调用 emit. 这违反了 flow 的 exception transparency 原则。要处理异常,可以调用 flow 的 <code>catch()</code> 方法,在 <code>catch() {}</code> 的 lambda 内针对具体的异常做处理。你可以:</p>
<ul>
<li>重新抛出这个异常;</li>
<li>用 <code>emit()</code> 把它交给外部处理;</li>
<li>也可以忽略,或者记录在日志里,或者用任何其它方式来处理异常。</li>
</ul>
<pre class="highlight"><code class="language-kotlin"><span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">e</span> <span class="p">-></span> <span class="nf">emit</span><span class="p">(</span><span class="s">"Caught $e"</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// emit on exception</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
</code></pre>
<p><code>catch()</code> 方法只能捕获上游的异常,无法捕获下游的异常。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Emitting $i"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">e</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Caught $e"</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// does not catch downstream exceptions</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">check</span><span class="p">(</span><span class="n">value</span> <span class="p"><=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="s">"Collected $value"</span> <span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>上面代码中的 <code>catch()</code> 就没有捕获 collector 里边的异常,要想达到效果,可以把抛出异常的代码放到 <code>onEach()</code> 里,并在 <code>catch()</code> 之前调用:</p>
<pre class="highlight"><code class="language-kotlin"><span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="nf">onEach</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="nf">check</span><span class="p">(</span><span class="n">value</span> <span class="p"><=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="s">"Collected $value"</span> <span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">e</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Caught $e"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span><span class="p">()</span>
</code></pre>
<h3>
<a id="Flow+completion" href="#Flow+completion" class="anchor"></a>Flow completion</h3>
<p>Flow 在结束时有时需要执行一些操作,这些操作可以放在 <code>finally</code> 块里:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>Flow 提供了另一个叫 <code>onCompletion()</code> 的方法来处理类似的情况:</p>
<pre class="highlight"><code class="language-kotlin"><span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="nf">onCompletion</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
</code></pre>
<p>它的 lambda 有一个 <code>Throwable?</code> 类型的参数,可以用于判断 flow 是正常结束还是异常结束,如果是正常结束,则该参数为 <code>null</code>.</p>
<p><code>onCompletion()</code> 方法不像 <code>catch()</code> 一样会拦截异常,异常仍然会被后面的 <code>catch()</code> 捕获:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">simple</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="nf">emit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">simple</span><span class="p">()</span>
<span class="p">.</span><span class="nf">onCompletion</span> <span class="p">{</span> <span class="n">cause</span> <span class="p">-></span> <span class="k">if</span> <span class="p">(</span><span class="n">cause</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Flow completed exceptionally"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">cause</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Caught exception"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">cause</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Caught exception again"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>至于使用 <code>try/finally</code> 还是 <code>onCompletion()</code> 的方式, 官方没有给出具体建议,由开发者根据需要自行选择。</p>
<h3>
<a id="Launching+flow" href="#Launching+flow" class="anchor"></a>Launching flow</h3>
<p>Flow 的 <code>onEach()</code> 方法可以起到一个类似 addEventListener 的作用,把一小段代码插入 flow 的处理逻辑当中。但是 <code>onEach()</code> 是一个 intermediate operator, 不会触发 flow 的执行,所以需要配合 <code>collect()</code> 方法:</p>
<pre class="highlight"><code class="language-kotlin"><span class="c1">// Imitate a flow of events</span>
<span class="k">fun</span> <span class="nf">events</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">3</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">onEach</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">events</span><span class="p">()</span>
<span class="p">.</span><span class="nf">onEach</span> <span class="p">{</span> <span class="n">event</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Event: $event"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">collect</span><span class="p">()</span> <span class="c1">// <--- Collecting the flow waits</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>Flow 提供了一个 <code>launchIn()</code> 方法,该方法作用和 <code>collect()</code> 类似,区别是 <code>launchIn()</code> 会在指定上下文里启动一个协程执行。所以它接受一个类型为 <code>CoroutineScope</code> 的参数,用于指定执行的环境。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">events</span><span class="p">()</span>
<span class="p">.</span><span class="nf">onEach</span> <span class="p">{</span> <span class="n">event</span> <span class="p">-></span> <span class="nf">println</span><span class="p">(</span><span class="s">"Event: $event"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">launchIn</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="c1">// <--- Launching the flow in a separate coroutine</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Done"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p><code>launchIn()</code> 的返回值是一个 <code>Job</code>, 你可以对其进行各种协程允许的操作,例如取消。</p>
<p>flow builder 会为每个 emit 自动执行 <code>ensureActive()</code> 用于检测取消动作,这意味着循环执行的 emit 是可以取消的。</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">foo</span><span class="p">():</span> <span class="nc">Flow</span><span class="p"><</span><span class="nc">Int</span><span class="p">></span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Emitting $i"</span><span class="p">)</span>
<span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="nf">foo</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="p">==</span> <span class="mi">3</span><span class="p">)</span> <span class="nf">cancel</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>但是出于性能的考量,多数的 flow operator 没有额外的取消动作检测,所以是无法取消的,例如:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="p">==</span> <span class="mi">3</span><span class="p">)</span> <span class="nf">cancel</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>在这种情况下,如果你想要让这个 flow 变得可取消,可以使用 <code>.onEach { currentCoroutineContext().ensureActive() }</code>,不过这个写法比较麻烦,写多了又成了重复代码,因此 Kotlin 提供了一个 <code>cancellable()</code> 方法:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="n">runBlocking</span><span class="p"><</span><span class="nc">Unit</span><span class="p">></span> <span class="p">{</span>
<span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">().</span><span class="nf">cancellable</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-></span>
<span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="p">==</span> <span class="mi">3</span><span class="p">)</span> <span class="nf">cancel</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<h2>
<a id="%E5%BC%82%E5%B8%B8" href="#%E5%BC%82%E5%B8%B8" class="anchor"></a>异常</h2>
<p>使用 <code>launch</code> 创建根协程(没有父协程的协程)时,协程内部抛出的异常被当作未捕获的异常 (uncaught exception) 来对待。使用 <code>async</code> 创建根协程时,异常的表现根据用户的使用方式而有所不同。</p>
<pre class="highlight"><code class="language-kotlin"><span class="nd">@OptIn</span><span class="p">(</span><span class="nc">DelicateCoroutinesApi</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span> <span class="c1">// root coroutine with launch</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Throwing exception from launch"</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">IndexOutOfBoundsException</span><span class="p">()</span> <span class="c1">// Will be printed to the console by Thread.defaultUncaughtExceptionHandler</span>
<span class="p">}</span>
<span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Joined failed job"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">deferred</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">async</span> <span class="p">{</span> <span class="c1">// root coroutine with async</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Throwing exception from async"</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">ArithmeticException</span><span class="p">()</span> <span class="c1">// Nothing is printed, relying on user to call await</span>
<span class="p">}</span>
<span class="k">try</span> <span class="p">{</span>
<span class="n">deferred</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Unreached"</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">ArithmeticException</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Caught ArithmeticException"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>上面的代码会输出</p>
<blockquote>
<p>Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException</p>
</blockquote>
<p>将代码中的 <code>await()</code> 调用改为 <code>join()</code>, 会发现异常同样无法捕获,但并没有中断后续的程序执行,之前没有输出的 "Unreached" 现在输出了。</p>
<p>对于 uncaught exception, Kotlin 默认的处理方式就是把异常栈打印出来。你可以通过 <code>CoroutineExceptionHandler</code> 来处理 uncaught exception, 从而改变这种默认处理方式,类似 Java 的 <code>Thread.uncaughtExceptionHandler</code>. 一般用于记录异常日志、显示一些错误信息、终止或者重启应用。</p>
<p>在 JVM 平台上,可以通过用 <code>ServiceLoader</code> 注册 <code>CoroutineExceptionHandler</code> 的方法重新定义 global exception handler. Global exception handler 类似 <code>Thread.defaultUncaughtExceptionHandler</code>, 没有特别指定 handler 时,会被默认使用。在 Android 平台上,<code>uncaughtExceptionPreHandler</code> 被用作 global exception handler.</p>
<p><code>CoroutineExceptionHandler</code> 仅在 uncaught exceptions 发生时被调用。实际上,所有子协程的异常处理会被代理给父协程,直至根协程。在子协程上设置 exception handler 是无效的,不会被使用。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler got $exception"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 根协程,运行在 GlobalScope 里</span>
<span class="k">throw</span> <span class="nc">AssertionError</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">deferred</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">async</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 同样是根协程,但这里用的是 async</span>
<span class="k">throw</span> <span class="nc">ArithmeticException</span><span class="p">()</span> <span class="c1">// 没有打印出异常信息</span>
<span class="p">}</span>
<span class="nf">joinAll</span><span class="p">(</span><span class="n">job</span><span class="p">,</span> <span class="n">deferred</span><span class="p">)</span>
</code></pre>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler got $exception"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">handler2</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">e</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler 2 got $e"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">@OptIn</span><span class="p">(</span><span class="nc">DelicateCoroutinesApi</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">runBlocking</span> <span class="p">{</span>
<span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">launch</span><span class="p">(</span><span class="n">handler2</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// exception handler 设置在子协程里无效</span>
<span class="k">throw</span> <span class="nc">AssertionError</span><span class="p">()</span>
<span class="p">}.</span><span class="nf">join</span><span class="p">()</span>
<span class="p">}.</span><span class="nf">join</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>实际上,把上面示例里的 <code>GlobalScope.launch</code> 的参数移除,子协程里的 handler2 仍然是无效的。同时这里的根协程如果不调用 <code>join</code> 方法,同样不会打印异常信息。</p>
<p>前面提到过,协程的取消会抛出 <code>CancellationException</code>, 这些异常会被所有的 exception handler 忽略。它们只应该在 debug 时的 <code>catch</code> 块里用作 debug 信息。当一个协程被调用 <code>Job.cancel()</code> 取消时,这个协程会终止,但是不会波及它的父协程。</p>
<p>当某个协程内部抛出除 <code>CancellationException</code> 以外的异常时,这个异常会往上传播给父级协程,然后,父级协程将会:1, 取消它内部的所有其它子协程;2, 取消它自己;3, 将这个异常继续往上传播给该父级协程的父级协程。这样直达根协程。</p>
<p>当多个子协程抛出异常时,只有第一个抛出的异常会被处理,其余的异常会作为第一个异常的 suppressed exception.</p>
<pre class="highlight"><code class="language-kotlin"><span class="nd">@OptIn</span><span class="p">(</span><span class="nc">DelicateCoroutinesApi</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="nc">Long</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span> <span class="c1">// it gets cancelled when another sibling fails with IOException</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="k">throw</span> <span class="nc">ArithmeticException</span><span class="p">()</span> <span class="c1">// the second exception</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">IOException</span><span class="p">()</span> <span class="c1">// the first exception</span>
<span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="nc">Long</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="p">}</span>
</code></pre>
<p>根协程对异常进行处理的触发时机,是在所有子协程运行结束以后,下面是个示例:</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler got $exception"</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">launch</span> <span class="p">{</span> <span class="c1">// the first child</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="nc">Long</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="nf">withContext</span><span class="p">(</span><span class="nc">NonCancellable</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Children are cancelled, but exception is not handled until all children terminate"</span><span class="p">)</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The first child finished its non cancellable block"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span> <span class="c1">// the second child</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Second child throws an exception"</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">ArithmeticException</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>你会发现 exception handler 对于异常的处理发生在两个子协程都终止之后。</p>
<p>为了保证结构化并发的稳定,协程异常传播的这种行为无法被覆盖。这在某些情况下是有合适的,但是在另一些情况可能就不那么合适了。比如说在一个 UI 相关的 CoroutineScope 里,一旦子协程抛出异常,该 CoroutineScope 整个都会被取消,无法启动新的协程,UI 就失去了响应。</p>
<p>这种情况下,可以使用 <code>Job</code> 的另一种实现:<code>SupervisorJob</code>. 它跟普通的 <code>Job</code> 基本相同,区别在于,子协程的异常不会影响到父协程,这样自然也就不会波及同一父协程的其它子协程。而同时,父协程又能够控制所有的子协程。或者换一种说法:异常引起的协程取消只会向下传播,而不会向上传播。下面这个例子可以说明这一点:</p>
<pre class="highlight"><code class="language-kotlin"><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">supervisor</span> <span class="p">=</span> <span class="nc">SupervisorJob</span><span class="p">()</span>
<span class="nf">with</span><span class="p">(</span><span class="nc">CoroutineScope</span><span class="p">(</span><span class="n">coroutineContext</span> <span class="p">+</span> <span class="n">supervisor</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// launch the first child -- its exception is ignored for this example (don't do this in practice!)</span>
<span class="kd">val</span> <span class="py">firstChild</span> <span class="p">=</span> <span class="nf">launch</span><span class="p">(</span><span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="p">-></span> <span class="p">})</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The first child is failing"</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">AssertionError</span><span class="p">(</span><span class="s">"The first child is cancelled"</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// launch the second child</span>
<span class="kd">val</span> <span class="py">secondChild</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
<span class="n">firstChild</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="c1">// Cancellation of the first child is not propagated to the second child</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active"</span><span class="p">)</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nf">delay</span><span class="p">(</span><span class="nc">Long</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span>
<span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
<span class="c1">// But cancellation of the supervisor is propagated</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The second child is cancelled because the supervisor was cancelled"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// wait until the first child fails & completes</span>
<span class="n">firstChild</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Cancelling the supervisor"</span><span class="p">)</span>
<span class="n">supervisor</span><span class="p">.</span><span class="nf">cancel</span><span class="p">()</span>
<span class="n">secondChild</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>在上面的例子中,要是把 <code>supervisor.cancel()</code> 这一行注释掉,可以观察到 firstChild 虽然失败了,但是 secondChild 仍然在运行,supervisor 本身也在运行。</p>
<p><code>supervisorScope()</code> 方法与 <code>coroutineScope()</code> 方法相同,区别在于前者的 block 创建的是 <code>SupervisorJob</code>, 后者的 block 创建的是 <code>Job</code>.</p>
<p><code>SupervisorJob</code> 和普通的 <code>Job</code> 在异常处理上还有一个很大的区别是,前者由于子协程不会把异常传播到父协程,所以每个子协程得指定自己的 exception handler,根协程的 exception handler 不会对子协程的异常进行任何处理。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler got $exception"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">handler2</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">e</span> <span class="p">-></span>
<span class="nf">println</span><span class="p">(</span><span class="s">"CoroutineExceptionHandler 2 got $e"</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
<span class="nc">GlobalScope</span><span class="p">.</span><span class="nf">launch</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">supervisorScope</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">child</span> <span class="p">=</span> <span class="nf">launch</span><span class="p">(</span><span class="n">handler2</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The child throws an exception"</span><span class="p">)</span>
<span class="k">throw</span> <span class="nc">AssertionError</span><span class="p">()</span>
<span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The scope is completing"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}.</span><span class="nf">join</span><span class="p">()</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"The scope is completed"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre>
<p>把上例的 <code>supervisorScope()</code> 替换成 <code>coroutineScope()</code> 可以观察到不同的输出结果。</p>
<p>需要注意的是,supervisor job 只保证自己不向上传播异常,但当它的子协程是普通 job 时,某个子协程引发的异常还是会导致自身取消,从而继续取消它的所有子协程。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">val</span> <span class="py">scope</span> <span class="p">=</span> <span class="nc">CoroutineScope</span><span class="p">(</span><span class="nc">SupervisorJob</span><span class="p">())</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
<span class="nf">launch</span> <span class="p">{</span>
<span class="c1">// child job</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>上面的例子中,child job 就是一个普通的 job, 而不是 supervisor job. 如果你希望它是一个 supervisor job,就把它用 <code>supervisorScope(){}</code> 包起来,或者通过 <code>scope.launch{}</code> 来创建,或者传递一个 <code>SupervisorJob()</code> 到 <code>launch()</code> 方法里。</p>
<h2>
<a id="Android+%E4%B8%AD%E7%9A%84%E5%8D%8F%E7%A8%8B%E7%BC%96%E7%A8%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" href="#Android+%E4%B8%AD%E7%9A%84%E5%8D%8F%E7%A8%8B%E7%BC%96%E7%A8%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" class="anchor"></a>Android 中的协程编程最佳实践</h2>
<h3>
<a id="%E6%B3%A8%E5%85%A5+Dispatcher" href="#%E6%B3%A8%E5%85%A5+Dispatcher" class="anchor"></a>注入 Dispatcher</h3>
<p>在新建协程或者调用 <code>withContext()</code> 时不要硬编码 Dispatcher</p>
<pre class="highlight"><code class="language-kotlin"><span class="c1">// 注入 Dispatchers</span>
<span class="kd">class</span> <span class="nc">NewsRepository</span><span class="p">(</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">defaultDispatcher</span><span class="p">:</span> <span class="nc">CoroutineDispatcher</span> <span class="p">=</span> <span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadNews</span><span class="p">()</span> <span class="p">=</span> <span class="nf">withContext</span><span class="p">(</span><span class="n">defaultDispatcher</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 不要硬编码 Dispatchers</span>
<span class="kd">class</span> <span class="nc">NewsRepository</span> <span class="p">{</span>
<span class="c1">// 像上一个示例那样注入 Dispatcher, 不要直接使用 Dispatchers.Default</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadNews</span><span class="p">()</span> <span class="p">=</span> <span class="nf">withContext</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>使用依赖注入模式能让代码变得更容易测试。这样就可以在单元测试和 instrumentation 测试中使用 <code>TestCoroutineDispatcher</code>.</p>
<p><code>viewModelScope</code> 属性内部的 dispatcher 被硬编码成了 <code>Dispatchers.Main</code>, 测试时可以使用 <code>Dispatchers.setMain()</code> 方法将其替换成 <code>TestCoroutineDispatcher</code>. 详见<a href="https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471">这里</a>。</p>
<h3>
<a id="Suspend+%E6%96%B9%E6%B3%95%E5%BA%94%E8%AF%A5%E8%83%BD%E5%A4%9F%E5%9C%A8%E4%B8%BB%E7%BA%BF%E7%A8%8B%E5%BD%93%E4%B8%AD%E5%AE%89%E5%85%A8%E5%9C%B0%E8%B0%83%E7%94%A8" href="#Suspend+%E6%96%B9%E6%B3%95%E5%BA%94%E8%AF%A5%E8%83%BD%E5%A4%9F%E5%9C%A8%E4%B8%BB%E7%BA%BF%E7%A8%8B%E5%BD%93%E4%B8%AD%E5%AE%89%E5%85%A8%E5%9C%B0%E8%B0%83%E7%94%A8" class="anchor"></a>Suspend 方法应该能够在主线程当中安全地调用</h3>
<p>Suspend 方法应当是“主线程安全”( main-safety ) 的。如果一个类在协程里进行长时间的阻塞操作,应该使用 <code>withContext()</code> 方法把操作从主线程当中移除。这个原则适用于 APP 当中所有的类。</p>
<pre class="highlight"><code class="language-kotlin"><span class="kd">class</span> <span class="nc">NewsRepository</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">ioDispatcher</span><span class="p">:</span> <span class="nc">CoroutineDispatcher</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 这个操作是从服务器上获取新闻</span>
<span class="c1">// 它使用了一个阻塞的 HttpURLConnection</span>
<span class="c1">// 所以需要把操作移入一个 IO dispatcher,使之成为“主线程安全”的</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchLatestNews</span><span class="p">():</span> <span class="nc">List</span><span class="p"><</span><span class="nc">Article</span><span class="p">></span> <span class="p">{</span>
<span class="nf">withContext</span><span class="p">(</span><span class="n">ioDispatcher</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... implementation ... */</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 抓取最新的新闻以及相关联的作者</span>
<span class="kd">class</span> <span class="nc">GetLatestNewsWithAuthorsUseCase</span><span class="p">(</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">newsRepository</span><span class="p">:</span> <span class="nc">NewsRepository</span><span class="p">,</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">authorsRepository</span><span class="p">:</span> <span class="nc">AuthorsRepository</span>
<span class="p">)</span> <span class="p">{</span>
<span class="c1">// 因为 newsRepository 是“主线程安全”的</span>
<span class="c1">// 所以这个方法不需要将操作的协程移入另一个线程</span>
<span class="c1">// 该协程只是创建了一个 list,并往里添加元素</span>
<span class="k">suspend</span> <span class="k">operator</span> <span class="k">fun</span> <span class="nf">invoke</span><span class="p">():</span> <span class="nc">List</span><span class="p"><</span><span class="nc">ArticleWithAuthor</span><span class="p">></span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">news</span> <span class="p">=</span> <span class="n">newsRepository</span><span class="p">.</span><span class="nf">fetchLatestNews</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">response</span><span class="p">:</span> <span class="nc">List</span><span class="p"><</span><span class="nc">ArticleWithAuthor</span><span class="p">></span> <span class="p">=</span> <span class="nf">mutableEmptyList</span><span class="p">()</span>
<span class="k">for</span> <span class="p">(</span><span class="n">article</span> <span class="k">in</span> <span class="n">news</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">author</span> <span class="p">=</span> <span class="n">authorsRepository</span><span class="p">.</span><span class="nf">getAuthor</span><span class="p">(</span><span class="n">article</span><span class="p">.</span><span class="n">author</span><span class="p">)</span>
<span class="n">response</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ArticleWithAuthor</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">author</span><span class="p">))</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>该模式能让你的 APP 更具可扩展性,因为在调用 suspend 方法时不再需要考虑该为哪种工作场景使用哪个 dispatcher, 这是属于方法提供者该考虑的事情。</p>
<h3>
<a id="ViewModel+%E5%BA%94%E5%BD%93%E5%88%9B%E5%BB%BA%E5%8D%8F%E7%A8%8B%EF%BC%88%E8%80%8C%E4%B8%8D%E6%98%AF%E6%9A%B4%E9%9C%B2+suspend+%E6%96%B9%E6%B3%95%EF%BC%89" href="#ViewModel+%E5%BA%94%E5%BD%93%E5%88%9B%E5%BB%BA%E5%8D%8F%E7%A8%8B%EF%BC%88%E8%80%8C%E4%B8%8D%E6%98%AF%E6%9A%B4%E9%9C%B2+suspend+%E6%96%B9%E6%B3%95%EF%BC%89" class="anchor"></a>ViewModel 应当创建协程(而不是暴露 suspend 方法)</h3>
<pre class="highlight"><code class="language-kotlin"><span class="c1">// DO create coroutines in the ViewModel</span>
<span class="kd">class</span> <span class="nc">LatestNewsViewModel</span><span class="p">(</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">getLatestNewsWithAuthors</span><span class="p">:</span> <span class="nc">GetLatestNewsWithAuthorsUseCase</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">_uiState</span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p"><</span><span class="nc">LatestNewsUiState</span><span class="p">>(</span><span class="nc">LatestNewsUiState</span><span class="p">.</span><span class="nc">Loading</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">uiState</span><span class="p">:</span> <span class="nc">StateFlow</span><span class="p"><</span><span class="nc">LatestNewsUiState</span><span class="p">></span> <span class="p">=</span> <span class="n">_uiState</span>
<span class="k">fun</span> <span class="nf">loadNews</span><span class="p">()</span> <span class="p">{</span>
<span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">latestNewsWithAuthors</span> <span class="p">=</span> <span class="nf">getLatestNewsWithAuthors</span><span class="p">()</span>
<span class="n">_uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">LatestNewsUiState</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">latestNewsWithAuthors</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// Prefer observable state rather than suspend functions from the ViewModel</span>
<span class="kd">class</span> <span class="nc">LatestNewsViewModel</span><span class="p">(</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">getLatestNewsWithAuthors</span><span class="p">:</span> <span class="nc">GetLatestNewsWithAuthorsUseCase</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// DO NOT do this. News would probably need to be refreshed as well.</span>
<span class="c1">// Instead of exposing a single value with a suspend function, news should</span>
<span class="c1">// be exposed using a stream of data as in the code snippet above.</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadNews</span><span class="p">()</span> <span class="p">=</span> <span class="nf">getLatestNewsWithAuthors</span><span class="p">()</span>
<span class="p">}</span>
</code></pre>
// in CoroutineScope
launch {
val users = loadContributorsSuspend(req) // suspend 方法
upd...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/1333
2022-07-22T07:30:37Z
2022-10-28T15:00:27Z
关于 MithrilJS 的 m.request()
<p><code>m.request()</code> 的 API 目前定义如下:</p>
<pre class="highlight"><code class="language-javascript"><span class="nx">m</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
<span class="na">method</span><span class="p">:</span> <span class="p">...,</span>
<span class="na">url</span><span class="p">:</span> <span class="p">...,</span>
<span class="na">body</span><span class="p">:</span> <span class="p">...</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 2xx, 304 处理逻辑</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">functiuon</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 其余状态码处理逻辑</span>
<span class="p">})</span>
</code></pre>
<p>该 API 在响应状态码为 2xx 和 304 时,返回给前端程序员的是后端传过来的 JSON, 响应状态码本身以及响应头等其它信息都被丢掉了。所有其它状态码被当作请求失败,返回给前端程序员的是一个 <code>Error</code> 对象,其中包含响应体本身和响应状态码。</p>
<p>可能多数情况下这不是问题,但是如果前端在接收到成功的响应时,需要根据不同的状态码做不同的处理,就完全没有办法。</p>
<p>照我的理解,响应不能粗暴地直接这么一分为二。300, 301, 302 该算成功还是失败?2xx 的状态码也有许多种,就算都归为成功,也还是需要客户端区别对待,还不如一开始就不分。4xx 和 5xx 虽然被分别定义为客户端和服务器端错误,但跟 2xx 一样,还有许多细分的状态。所以作为框架,干脆就别定义所谓的成功和失败,把这个定义留给框架的使用者。</p>
<p>也就是说,<code>m.request()</code> 在收到响应之后,应该直接把原始的 <code>Response</code> 对象(或者稍微封装一下必要的信息)通过 fulfilled 状态的 <code>Promise</code> 返回给程序员,无论是什么状态的响应。而不需要用到 rejected 状态的 <code>Promise</code>, 如下:</p>
<pre class="highlight"><code class="language-javascript"><span class="nx">m</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
<span class="na">method</span><span class="p">:</span> <span class="p">...,</span>
<span class="na">url</span><span class="p">:</span> <span class="p">...,</span>
<span class="na">body</span><span class="p">:</span> <span class="p">...</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
<span class="p">})</span>
</code></pre>
<p>2022-08-01 补充:在 Mithril 中,<code>m.request()</code> 的调用会引起 Mithril 的界面重绘,<code>fetch()</code> 则不会。起初我认为把 <code>fetch()</code> 和 <code>m.redraw()</code> 的调用封装在一起就好了,但后来发现不可行,<code>fetch()</code> 方法返回一个 Promise, 简单的封装做不到保证让 <code>m.redraw()</code> 在最后一个 <code>.then()</code> 调用之后才执行。思来想去,还是放弃了 MithrilJS. 最近在研究 SolidJS.</p>
m.request() 的 API 目前定义如下:
m.request({
method: ...,
url: ...,
body: ...
})
.then(function(re...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/796
2022-04-11T09:55:15Z
2022-10-28T15:07:10Z
Ruby 代码线程安全的一些编写原则
<h1>
<a id="%E9%81%BF%E5%85%8D%E4%BF%AE%E6%94%B9%E5%85%A8%E5%B1%80%E5%85%B1%E4%BA%AB%E7%9A%84%E5%AF%B9%E8%B1%A1" href="#%E9%81%BF%E5%85%8D%E4%BF%AE%E6%94%B9%E5%85%A8%E5%B1%80%E5%85%B1%E4%BA%AB%E7%9A%84%E5%AF%B9%E8%B1%A1" class="anchor"></a>避免修改全局共享的对象</h1>
<p>非必要的情况下<strong>尽量避免修改全局共享的对象</strong>,包括 <code>$</code> 开头的全局变量、单实例对象、AST、类变量/方法等。</p>
<p>下面的写法是线程安全的,因为它没有修改全局的状态:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">class</span> <span class="nc">RainyCloudFactory</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">generate</span>
<span class="n">cloud</span> <span class="o">=</span> <span class="no">Cloud</span><span class="p">.</span><span class="nf">new</span>
<span class="n">cloud</span><span class="p">.</span><span class="nf">rain!</span>
<span class="n">cloud</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>下面的写法不是线程安全的,因为它修改了全局的状态:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">class</span> <span class="nc">RainyCloudFactory</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">generate</span>
<span class="n">cloud</span> <span class="o">=</span> <span class="no">Cloud</span><span class="p">.</span><span class="nf">new</span>
<span class="vc">@@clouds</span> <span class="o"><<</span> <span class="n">cloud</span>
<span class="n">cloud</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>但是,如果非用全局共享的对象不可,也不是不能用,只要保证线程安全即可,例如:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'thread'</span>
<span class="k">class</span> <span class="nc">Counter</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@counter</span> <span class="o">=</span> <span class="mi">0</span>
<span class="vi">@mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">increment</span>
<span class="vi">@mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="vi">@counter</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="vg">$counter</span> <span class="o">=</span> <span class="no">Counter</span><span class="p">.</span><span class="nf">new</span>
</code></pre>
<h2>
<a id="AST" href="#AST" class="anchor"></a>AST</h2>
<p>上面提到的 AST 指的是程序的抽象语法树里的指令,Ruby 作为一门动态语言,是允许在运行时修改它的。所有的线程共享一份 AST, 因此在多线程环境下,同样存在线程安全的问题——虽然很罕见。kaminari 这个 Gem 曾经就出过这样的一个问题:<a href="https://github.com/kaminari/kaminari/issues/214">https://github.com/kaminari/kaminari/issues/214</a></p>
<p>代码里动态地定义了一个方法,然后用 <code>alias_method</code> 为其创建别名,接着又删除了初始定义的那个方法。在多线程环境下,就会出现这样的情况:线程 A 定义了一个方法,并且为其创建了别名。接着线程 B 也定义了这个方法,覆盖了线程 A 定义的方法。然后线程 A 删除了该方法。线程 B 再为它创建别名时,就抛出了 <code>NoMethodError</code> 异常。</p>
<p>所以,为了线程安全,应该<strong>避免在运行时修改 AST</strong>. 这里的“运行时”,指的是应用程序运行的过程中。或者换句话说,<strong>AST 的修改应该在程序的启动过程中完成</strong>。</p>
<h1>
<a id="%E5%88%9B%E5%BB%BA%E6%9B%B4%E5%A4%9A%E7%9A%84%E5%AF%B9%E8%B1%A1%EF%BC%8C%E8%80%8C%E4%B8%8D%E8%A6%81%E5%85%B1%E4%BA%AB%E5%90%8C%E4%B8%80%E4%B8%AA" href="#%E5%88%9B%E5%BB%BA%E6%9B%B4%E5%A4%9A%E7%9A%84%E5%AF%B9%E8%B1%A1%EF%BC%8C%E8%80%8C%E4%B8%8D%E8%A6%81%E5%85%B1%E4%BA%AB%E5%90%8C%E4%B8%80%E4%B8%AA" class="anchor"></a>创建更多的对象,而不要共享同一个</h1>
<p>有时候我们免不了还是需要使用全局对象,例如数据库连接。这时候有两种办法解决线程安全问题。</p>
<h2>
<a id="Thread-locals" href="#Thread-locals" class="anchor"></a>Thread-locals</h2>
<p>Thread-locals 的方案,是让对象仅在当前线程里全局可见。下面是个例子:</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># 把这样的代码:</span>
<span class="vg">$redis</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span>
<span class="c1"># 用这种方式替代:</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">current</span><span class="p">[</span><span class="ss">:redis</span><span class="p">]</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span>
</code></pre>
<p>Thread-locals 的方案会为每个线程创建一个对应实例。但是当线程数很大的时候,资源池可能是一个更好的方案。</p>
<h2>
<a id="%E8%B5%84%E6%BA%90%E6%B1%A0" href="#%E8%B5%84%E6%BA%90%E6%B1%A0" class="anchor"></a>资源池</h2>
<p>假设有 N 个线程需要连接 Redis, 连接池里有 M 个连接可供使用,M < N. 这种情况下资源池仍然能够保证线程安全。</p>
<p>一个资源池通常维护着一定数量的资源供多个线程使用。当一个线程需要使用该资源(比如 Redis 连接)时,资源池会取出一个资源供其使用,并在使用完毕后将该资源放回池中以待下一个线程使用。这样保证了线程安全。</p>
<h1>
<a id="%E9%81%BF%E5%85%8D%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD" href="#%E9%81%BF%E5%85%8D%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD" class="anchor"></a>避免延迟加载</h1>
<p>在 3.0 版本之前,Ruby on Rails 中的一个习惯用法是在运行时加载常量,用的是类似 Ruby 的 <code>autoload</code> 的方法。但这不是个好的做法,因为 MRI Ruby 的 <code>autoload</code> 并不是线程安全的。虽然在现在的 JRuby 里,<code>autoload</code> 是线程安全的,但最好的做法还是在派生 worker 线程之前预先加载需要的常量。</p>
<h1>
<a id="%E4%BC%98%E5%85%88%E4%BD%BF%E7%94%A8%EF%BC%88%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%89%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%80%8C%E4%B8%8D%E6%98%AF+Mutex" href="#%E4%BC%98%E5%85%88%E4%BD%BF%E7%94%A8%EF%BC%88%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%89%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%80%8C%E4%B8%8D%E6%98%AF+Mutex" class="anchor"></a>优先使用(线程安全的)数据结构而不是 Mutex</h1>
<p>Mutex 不容易正确地使用。在使用 Mutex 的时候你要考虑许多问题:</p>
<ul>
<li>这个 Mutex 的粒度应该有多大?</li>
<li>这里会不会发生死锁?</li>
<li>我应该为每个对象创建一个 Mutex 还是使用全局的 Mutex 对象?</li>
</ul>
<p>以上问题只是需要考虑的其中一部分。当然对于熟练掌握 Mutex 用法的程序员来说,这些问题不难回答。</p>
<p>但使用数据结构(例如 Queue)可以免掉这些考量。与其担心这些问题,还不如干脆不用 Mutex.</p>
<p>另外,这里还有一些来自 JRuby wiki 的关于编写并发代码的“安全路径”:</p>
<ul>
<li>不要编写并发代码,除非无法避免;</li>
<li>如果你非得编写并发代码不可,不要在线程间共享数据;</li>
<li>如果非得在线程间共享数据不可,不要共享可变数据;</li>
<li>如果非共享可变数据不可,在访问这些数据时做同步处理。</li>
</ul>
避免修改全局共享的对象
非必要的情况下尽量避免修改全局共享的对象,包括 $ 开头的全局变量、单实例对象、AST、类变量/方法等。
下面的写法是线程安全的,因为它没有修改全局的状态:
class...
yuan
https://geeknote.net/yuan
https://geeknote.net/yuan/posts/596
2022-04-07T15:03:28Z
2022-10-28T14:46:24Z
Ruby 线程基础
<p>本文是学习笔记,学习过程中主要阅读和参考了以下资料,记录的代码片断也来自以下链接。部分代码稍作了修改。最后那个链接虽然有些标题党,但是内容很值得一看:</p>
<ul>
<li><a href="https://workingwithruby.com/wwrt/intro/">Working with Ruby Threads</a></li>
<li><a href="http://www.rubyinside.com/does-the-gil-make-your-ruby-code-thread-safe-6051.html">Does the GIL Make Your Ruby Code Thread-Safe?</a></li>
<li><a href="https://vaneyckt.io/posts/ruby_concurrency_in_praise_of_the_mutex/">Ruby concurrency: in praise of the mutex</a></li>
<li><a href="https://www.modb.pro/db/126045">99%的人没弄懂volatile的设计原理,更别说灵活运用了</a></li>
</ul>
<hr>
<p>如果你想要靠并发提高性能,就得开启更多的进程,Ruby 社区多年以来一直是这么做的。</p>
<p>但是线程的内存开销比进程小得多,因为同一进程内的线程可以共享内存,而进程之间并不共享内存。而使用线程代价是:你的程序必须是线程安全的。</p>
<p>我们的代码始终运行在线程中。在没有主动创建线程的情况下,代码运行在主线程中。</p>
<p>主线程与其它线程的一个主要区别是,当主线程退出时,其它线程会立即退出,并且当前 Ruby 进程也会退出。</p>
<p><code>Thread.main</code> 用于获取主线程。<code>Thread.current</code> 用于获取当前线程。</p>
<h1>
<a id="%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%89%A7%E8%A1%8C" href="#%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%89%A7%E8%A1%8C" class="anchor"></a>线程的执行</h1>
<p>多线程中最重要的概念就是共享地址空间,这是多线程的优点,同时也是多线程编程困难的原因。</p>
<p>1.8 及之前版本的 Ruby, 一直使用的是“绿色线程”,即 Ruby 虚拟机自己管理的线程。Ruby 1.9 之后,一个 Ruby 线程直接映射到一个操作系统线程(当你在 Ruby 进程里创建线程时,可以用 top 命令看到它),由操作系统调度。</p>
<p>为公平起见,线程调度器会在任意时刻挂起当前线程,切换到另一个线程执行程序。之后在某个时间,调度器又会切回到之前的线程继续执行。这个动作叫做“上下文切换”(context switching)。</p>
<p>下面的代码演示了“上下文切换”可能引起的问题。其中的 <code>||=</code> 不是原子性操作,因此不是线程安全的。所谓“原子操作”,是指在它执行完成之前不会被上下文切换中断的操作。</p>
<pre class="highlight"><code class="language-ruby"><span class="vi">@results</span> <span class="o">||=</span> <span class="no">Queue</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@results</span> <span class="o"><<</span> <span class="n">status</span>
</code></pre>
<p>以上代码大约等价于:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">if</span> <span class="vi">@results</span><span class="p">.</span><span class="nf">nil?</span>
<span class="n">temp</span> <span class="o">=</span> <span class="no">Queue</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@results</span> <span class="o">=</span> <span class="n">temp</span>
<span class="k">end</span>
<span class="vi">@results</span> <span class="o"><<</span> <span class="n">status</span>
</code></pre>
<p>因此 <code>||=</code> 有可能造成在一个线程执行完 <code>@results.nil?</code> 的判断之后挂起,切换到另一个线程。另一个线程执行完整个赋值步骤并往 <code>@results</code> 里塞数据之后再切换回来,这样便造成给 <code>@results</code> 二次赋值。第一次赋值后塞进的数据被丢掉。这种情况下,我们说这里产生了“竞态条件” (race condition).</p>
<p>要解决 <code>||=</code> 引起的线程安全问题,就应该在初始化<strong>公共资源</strong>时避免使用它,改成在构造方法里或者用其它方式初始化该变量。</p>
<h1>
<a id="%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" href="#%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="anchor"></a>线程的生命周期</h1>
<p><code>Thread.new</code> 用于创建一个线程,block 参数内的代码块即为新线程里运行的代码。</p>
<p><code>join</code> 方法的调用会让当前线程等待被调用 <code>join</code> 方法的那个线程实例执行完毕,之后接着执行当前线程。</p>
<p>两个不同的线程当中,任意一个线程抛出异常,都会中止自身线程,但都不影响另一个线程继续执行。但是如果在一个线程里调用了另一个线程的 <code>join</code> 方法,则前者会受后者未处理异常的影响。以下两段代码的执行结果会有所不同:</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># 代码片断 1</span>
<span class="mi">3</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="k">raise</span> <span class="s1">'error'</span> <span class="p">}</span>
<span class="k">end</span>
<span class="nb">sleep</span> <span class="mi">1</span>
<span class="nb">puts</span> <span class="s1">'end'</span>
<span class="c1"># 代码片断 2</span>
<span class="mi">3</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="k">raise</span> <span class="s1">'error'</span> <span class="p">}</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">sleep</span> <span class="mi">1</span>
<span class="nb">puts</span> <span class="s1">'end'</span>
</code></pre>
<p><code>value</code> 方法与 <code>join</code> 方法类似,但它会返回线程的执行结果。</p>
<pre class="highlight"><code class="language-ruby"><span class="n">thread</span> <span class="o">=</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="mi">400</span> <span class="o">+</span> <span class="mi">5</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="n">thread</span><span class="p">.</span><span class="nf">value</span> <span class="c1">#=> 405</span>
</code></pre>
<p><code>status</code> 方法可以获取线程的状态。Ruby 为线程定义了如下几种状态:</p>
<ul>
<li>run: 表示线程正在运行;</li>
<li>sleep: 表示线程正在睡眠、被 mutex 阻塞或者在等待 IO.</li>
<li>false: 表示线程执行完毕,或者被成功地杀掉;</li>
<li>nil: 表示线程内部抛出了异常并且未被捕获和处理;</li>
<li>aborting: 线程正在退出</li>
</ul>
<pre class="highlight"><code class="language-ruby"><span class="n">adder</span> <span class="o">=</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="c1"># Here this thread checks its own status.</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">current</span><span class="p">.</span><span class="nf">status</span> <span class="c1">#=> 'run'</span>
<span class="mi">2</span> <span class="o">*</span> <span class="mi">3</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="n">adder</span><span class="p">.</span><span class="nf">status</span> <span class="c1">#=> 'run'</span>
<span class="n">adder</span><span class="p">.</span><span class="nf">join</span>
<span class="nb">puts</span> <span class="n">adder</span><span class="p">.</span><span class="nf">status</span> <span class="c1">#=> false</span>
</code></pre>
<p><code>Thread.stop</code> 方法可以让线程睡眠,之后线程调度器会去调度其它线程。<code>wakeup</code> 方法可以将其唤醒。注意前者是类方法。</p>
<pre class="highlight"><code class="language-ruby"><span class="n">thread</span> <span class="o">=</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">stop</span>
<span class="nb">puts</span> <span class="s1">'Hello there'</span>
<span class="k">end</span>
<span class="c1"># wait for the thread to trigger its stop</span>
<span class="k">begin</span>
<span class="nb">puts</span> <span class="n">thread</span><span class="p">.</span><span class="nf">status</span>
<span class="k">end</span> <span class="k">until</span> <span class="n">thread</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="s1">'sleep'</span>
<span class="n">thread</span><span class="p">.</span><span class="nf">wakeup</span>
<span class="n">thread</span><span class="p">.</span><span class="nf">join</span>
</code></pre>
<p><code>Thread.pass</code> 方法直接让调度器去调度其它线程,但当前线程自身并不睡眠——也因此调度器并不一定会照字面上的意图去调度其它线程。</p>
<p><code>raise</code>, <code>kill</code> 方法:不要使用。</p>
<h1>
<a id="%E5%B9%B6%E5%8F%91+%21%3D+%E5%B9%B6%E8%A1%8C" href="#%E5%B9%B6%E5%8F%91+%21%3D+%E5%B9%B6%E8%A1%8C" class="anchor"></a>并发 != 并行</h1>
<p>两个线程同时分别开始干两件事情,这叫做并发。但在单核 CPU 的环境下,两个并发线程的代码是交替执行的,并不算并行;而在多核心 CPU 的环境下,两个并发线程的代码有可能真正意义上的“同时”进行,这才叫并行。但这也只是有可能,具体是不是并行,还要看线程调度器如何调度。因此,并发的代码是否能够并行,这不是程序员的代码能够控制的。</p>
<h1>
<a id="GIL+%E5%92%8C+MRI" href="#GIL+%E5%92%8C+MRI" class="anchor"></a>GIL 和 MRI</h1>
<p>因为 GIL 的存在,MRI 允许 Ruby 代码的执行并发,但不允许并行。JRuby 和 Rubinius 没有 GIL,所以允许代码并行执行。</p>
<p>GIL (Globel Interpreter Lock) 有时又被叫做 GVL (Global VM Lock), 是一个全局锁——每个 MRI Ruby 进程有且只有一个 GIL. 也就是说,进程中的所有线程共享一个 GIL. 只有正在执行的那个线程能够获得这个锁,其它线程则会等待,直到轮到它们获取 GIL. 因此,MRI Ruby 里不存在并行。</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'digest/md5'</span>
<span class="mi">3</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="no">Digest</span><span class="o">::</span><span class="no">MD5</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="nb">rand</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:value</span><span class="p">)</span>
</code></pre>
<p>以上代码创建了 3 个线程用来生成一个随机数的 MD5 值。因为 3 个线程都想要执行,所以他们都会去尝试获取 GIL. GIL 是用互斥锁 ( mutex ) 实现的。操作系统会保证在任意时刻只有一个线程持有互斥锁,其余线程则进入睡眠状态,直到互斥锁被释放,再次变得可用。</p>
<p>获得 GIL 的线程(这里我们称之为线程 A)此时开始执行代码,具体执行多久由 MRI 内部实现。一段时间之后,该线程释放 GIL, 线程调度器唤醒另外两个线程,开始争夺 GIL. 系统内核会决定谁将获得 GIL. 这时,线程 A 和另一个未获取到 GIL 的线程进入睡眠状态。</p>
<h2>
<a id="%E7%89%B9%E6%AE%8A%E6%83%85%E5%86%B5%EF%BC%9A%E9%98%BB%E5%A1%9E+IO" href="#%E7%89%B9%E6%AE%8A%E6%83%85%E5%86%B5%EF%BC%9A%E9%98%BB%E5%A1%9E+IO" class="anchor"></a>特殊情况:阻塞 IO</h2>
<p>GIL 能够阻止 Ruby 代码并行执行,但是阻塞 IO 执行的不是 Ruby 代码:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'open-uri'</span>
<span class="mi">3</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="nb">open</span><span class="p">(</span><span class="s1">'http://zombo.com'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:value</span><span class="p">)</span>
</code></pre>
<p>这段代码中创建了 3 个线程发出 HTTP 请求,MRI 在遇到阻塞 IO 时不会让线程继续占用 GIL, 这样其它线程就可以获得 GIL 以继续执行 Ruby 代码,于是就有第二个、第三个线程跟着执行到此处,同时等待着 HTTP 的响应。</p>
<h2>
<a id="%E4%B8%80%E4%BA%9B%E8%AF%AF%E8%A7%A3" href="#%E4%B8%80%E4%BA%9B%E8%AF%AF%E8%A7%A3" class="anchor"></a>一些误解</h2>
<p>GIL 的存在并不能保证线程安全。比如有下面一段代码:</p>
<pre class="highlight"><code class="language-ruby"><span class="vi">@counter</span> <span class="o">=</span> <span class="mi">0</span>
<span class="mi">5</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">temp</span> <span class="o">=</span> <span class="vi">@counter</span>
<span class="n">temp</span> <span class="o">=</span> <span class="n">temp</span> <span class="o">+</span> <span class="mi">1</span>
<span class="vi">@counter</span> <span class="o">=</span> <span class="n">temp</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">puts</span> <span class="vi">@counter</span>
</code></pre>
<p>在 MRI 环境中,几乎不会出现什么错误,但是如果在线程里插入一句 <code>puts</code>, 情况就不一样了。如上面所说,阻塞 IO 会让线程释放 GIL, 这样就可能出问题:</p>
<pre class="highlight"><code class="language-ruby"><span class="vi">@counter</span> <span class="o">=</span> <span class="mi">0</span>
<span class="mi">5</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">temp</span> <span class="o">=</span> <span class="vi">@counter</span>
<span class="n">temp</span> <span class="o">=</span> <span class="n">temp</span> <span class="o">+</span> <span class="mi">1</span>
<span class="nb">puts</span> <span class="s1">'message'</span>
<span class="vi">@counter</span> <span class="o">=</span> <span class="n">temp</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">puts</span> <span class="vi">@counter</span>
</code></pre>
<h1>
<a id="%E5%A4%9A%E5%B0%91%E7%BA%BF%E7%A8%8B%E6%89%8D%E7%AE%97%E5%A4%AA%E5%A4%9A%EF%BC%9F" href="#%E5%A4%9A%E5%B0%91%E7%BA%BF%E7%A8%8B%E6%89%8D%E7%AE%97%E5%A4%AA%E5%A4%9A%EF%BC%9F" class="anchor"></a>多少线程才算太多?</h1>
<h2>
<a id="1.+%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E9%99%90%E5%88%B6" href="#1.+%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E9%99%90%E5%88%B6" class="anchor"></a>1. 操作系统的限制</h2>
<pre class="highlight"><code class="language-ruby"><span class="mi">1</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="mi">10_000</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="nb">sleep</span> <span class="p">}</span>
<span class="nb">puts</span> <span class="n">i</span>
<span class="k">end</span>
</code></pre>
<p>上面这段代码,在 OS X 系统当中,只能输出到 2000 左右,随后便会抛出 <code>ThreadError</code> 异常中断执行。因为 OS X 对单个进程的最大线程数作出了限制。但在 Linux 当中,这段代码可以完整地执行完。</p>
<h2>
<a id="2.+IO+%E5%AF%86%E9%9B%86%E5%9E%8B%E7%9A%84%E4%BB%A3%E7%A0%81" href="#2.+IO+%E5%AF%86%E9%9B%86%E5%9E%8B%E7%9A%84%E4%BB%A3%E7%A0%81" class="anchor"></a>2. IO 密集型的代码</h2>
<p>网络请求、日志输出等任务是属于 IO 密集型的任务,在处理这类任务时,提升网络速度、磁盘读写速度可以很大地提高整体性能。如果你的代码是 IO 密集型的,线程越多运行得可能越快,而且往往线程数多于 CPU 核心数是有作用的。但也不是越多越好。比如下面这段代码:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'benchmark'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="no">URL</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s1">'http://www.baidu.com/'</span><span class="p">)</span>
<span class="no">ITERATIONS</span> <span class="o">=</span> <span class="mi">30</span>
<span class="k">def</span> <span class="nf">fetch_url</span><span class="p">(</span><span class="n">thread_count</span><span class="p">)</span>
<span class="n">threads</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">thread_count</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">threads</span> <span class="o"><<</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">fetches_per_thread</span> <span class="o">=</span> <span class="no">ITERATIONS</span> <span class="o">/</span> <span class="n">thread_count</span>
<span class="n">fetches_per_thread</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="no">URL</span><span class="p">)</span>
<span class="k">rescue</span> <span class="o">=></span> <span class="n">e</span>
<span class="k">retry</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">threads</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">Benchmark</span><span class="p">.</span><span class="nf">bm</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">bm</span><span class="o">|</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">15</span><span class="p">,</span> <span class="mi">30</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">thread_count</span><span class="o">|</span>
<span class="n">bm</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"with </span><span class="si">#{</span><span class="n">thread_count</span><span class="si">}</span><span class="s2"> threads"</span><span class="p">)</span> <span class="k">do</span>
<span class="n">fetch_url</span><span class="p">(</span><span class="n">thread_count</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>在我的电脑上运行结果如下:</p>
<pre class="highlight"><code> user system total real
with 1 threads 6.610000 0.160000 6.770000 ( 4.259840)
with 2 threads 3.630000 0.060000 3.690000 ( 2.390968)
with 3 threads 1.000000 0.040000 1.040000 ( 1.494768)
with 5 threads 1.190000 0.060000 1.250000 ( 1.392179)
with 6 threads 0.790000 0.030000 0.820000 ( 0.989476)
with 10 threads 0.830000 0.030000 0.860000 ( 0.673836)
with 15 threads 0.940000 0.020000 0.960000 ( 0.654832)
with 30 threads 0.780000 0.090000 0.870000 ( 0.653245)
</code></pre>
<p>基本上 10 个线程就已经达到最佳性能。再往上加线程数就不划算了,因为线程的创建也有开销——虽然比进程开销小。因此,到底多少线程合适,还得视具体情况而定。如果网络延迟较高,可能会需要更多的线程达到最佳性能。比如我把上面代码中的网址换成一个俄罗斯的网址,执行结果就变成:</p>
<pre class="highlight"><code> user system total real
with 1 threads 17.030000 0.340000 17.370000 ( 22.337289)
with 2 threads 5.110000 0.140000 5.250000 ( 10.433548)
with 3 threads 2.380000 0.070000 2.450000 ( 6.352316)
with 5 threads 1.860000 0.090000 1.950000 ( 4.780961)
with 6 threads 2.030000 0.070000 2.100000 ( 3.578058)
with 10 threads 1.890000 0.030000 1.920000 ( 2.331823)
with 15 threads 1.530000 0.060000 1.590000 ( 1.875414)
with 30 threads 1.470000 0.050000 1.520000 ( 1.350604)
</code></pre>
<p>另外,这段代码无论是用 MRI Ruby 还是 JRuby, 运行结果都差不多,也说明了阻塞 IO 会让线程释放 GIL.</p>
<h2>
<a id="3.+CPU+%E5%AF%86%E9%9B%86%E5%9E%8B%E7%9A%84%E4%BB%A3%E7%A0%81" href="#3.+CPU+%E5%AF%86%E9%9B%86%E5%9E%8B%E7%9A%84%E4%BB%A3%E7%A0%81" class="anchor"></a>3. CPU 密集型的代码</h2>
<p>压缩/解压、加密、计算散列值以及一些非常复杂的数学计算之类的任务属于 CPU 密集型任务。在处理这类任务时,CPU 频率的提升能够提高整体性能。线程数不超过核心数时,线程越多运行越快。线程数超过核心数之后,性能不再提升,反而可能因为线程切换的开销有所下降。</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'benchmark'</span>
<span class="no">DIGITS</span> <span class="o">=</span> <span class="mi">30</span>
<span class="no">ITERATIONS</span> <span class="o">=</span> <span class="mi">24</span>
<span class="k">def</span> <span class="nf">fibonacci</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
<span class="k">return</span> <span class="n">n</span> <span class="k">if</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">1</span><span class="p">).</span><span class="nf">include?</span> <span class="n">n</span>
<span class="p">(</span><span class="n">fibonacci</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">fibonacci</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">2</span><span class="p">))</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">calculate_fib</span><span class="p">(</span><span class="n">thread_count</span><span class="p">)</span>
<span class="n">threads</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">thread_count</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">threads</span> <span class="o"><<</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">iterations_per_thread</span> <span class="o">=</span> <span class="no">ITERATIONS</span> <span class="o">/</span> <span class="n">thread_count</span>
<span class="n">iterations_per_thread</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">fibonacci</span><span class="p">(</span><span class="no">DIGITS</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">threads</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">Benchmark</span><span class="p">.</span><span class="nf">bm</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">bm</span><span class="o">|</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">24</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">thread_count</span><span class="o">|</span>
<span class="n">bm</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"with </span><span class="si">#{</span><span class="n">thread_count</span><span class="si">}</span><span class="s2"> threads"</span><span class="p">)</span> <span class="k">do</span>
<span class="n">calculate_fib</span><span class="p">(</span><span class="n">thread_count</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>这段代码在我的四核 CPU 上执行的结果如下:</p>
<pre class="highlight"><code> user system total real
with 1 threads 5.920000 0.050000 5.970000 ( 5.102896)
with 2 threads 4.980000 0.010000 4.990000 ( 2.444580)
with 3 threads 4.880000 0.000000 4.880000 ( 1.626186)
with 4 threads 5.380000 0.000000 5.380000 ( 1.355286)
with 6 threads 7.070000 0.000000 7.070000 ( 1.336971)
with 8 threads 10.130000 0.030000 10.160000 ( 1.317468)
with 12 threads 9.990000 0.010000 10.000000 ( 1.319313)
with 24 threads 10.400000 0.020000 10.420000 ( 1.373466)
</code></pre>
<p>看起来线程数与核心数相同时就接近最佳性能了。</p>
<p>在真实系统当中,很少有纯粹的 IO 密集或者 CPU 密集型程序。拿常见的 web 服务来举例,当它在接收客户端请求、返回响应内容、读写数据库时,做的就是 IO 密集型的事情;当它在解析数据、进行数学计算、渲染 HTML, JSON 的时候,做的就是 CPU 密集型的工作。所以,到底多少线程是合适的,还是需要具体地测试才能得出结论。</p>
<h1>
<a id="%E7%94%A8+Mutex+%E4%BF%9D%E6%8A%A4%E6%95%B0%E6%8D%AE" href="#%E7%94%A8+Mutex+%E4%BF%9D%E6%8A%A4%E6%95%B0%E6%8D%AE" class="anchor"></a>用 Mutex 保护数据</h1>
<p>Ruby 标准库当中的 <code>Array</code> 不是线程安全的:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">shared_array</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span>
<span class="n">mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="mi">1000</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">shared_array</span> <span class="o"><<</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">shared_array</span><span class="p">.</span><span class="nf">size</span>
</code></pre>
<p>以上代码将会产生类似以下输出:</p>
<pre class="highlight"><code>$ ruby code/snippets/concurrent_array_pushing.rb
10000
$ jruby code/snippets/concurrent_array_pushing.rb
7521
$ rbx code/snippets/concurrent_array_pushing.rb
8541
$ _
</code></pre>
<p>这里可以使用 <code>Mutex</code> (互斥锁)来保证正确的结果:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">shared_array</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span>
<span class="n">mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="mi">1000</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">lock</span>
<span class="n">shared_array</span> <span class="o"><<</span> <span class="kp">nil</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">unlock</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">shared_array</span><span class="p">.</span><span class="nf">size</span>
</code></pre>
<p>Mutex 在这里的使用,告诉了线程调度器在当前代码块执行完之前别切换线程。其中 <code>lock ... unlock</code> 代码块也可以改成这样:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">shared_array</span> <span class="o"><<</span> <span class="kp">nil</span>
<span class="k">end</span>
</code></pre>
<h2>
<a id="Mutex+%E5%92%8C%E5%86%85%E5%AD%98%E5%8F%AF%E8%A7%81%E6%80%A7" href="#Mutex+%E5%92%8C%E5%86%85%E5%AD%98%E5%8F%AF%E8%A7%81%E6%80%A7" class="anchor"></a>Mutex 和内存可见性</h2>
<p>线程 A 在持有 mutex 并对变量进行了修改,线程 B 直接读取该变量的话,有可能读到的是修改之前的值。</p>
<p>我们知道在计算机硬件结构当中,早期的 CPU 是直接读写内存的。后来为了提升性能,CPU 加上了高速缓存(一级缓存、二级缓存等),直接在高速缓存里读写数据,运算完成后再把数据同步到内存。在单核时代这并不会有什么问题,但在多核 CPU 中会有内存一致性的问题,因为不同 CPU 核心使用的是独立的一、二级缓存,并不共享。</p>
<p>因此,即使用了 mutex, 线程 B 也有可能在该变量未同步到内存时读取了它的值,造成我们不想要的结果。为了避免这个问题,需要用到一个叫作“内存屏障”的东西,它可以对内存操作进行排序约束,让变量的写操作执行完毕并同步到内存之后再开始读操作,从而防止读操作读到旧数据。</p>
<p>Ruby 的 Mutex 在充当原子性锁的同时,也是一个内存屏障。只要修改一下变量读取的方式,你就可以利用好这个内存屏障。下面是一个例子:</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># visibility.rb</span>
<span class="n">mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="n">flags</span> <span class="o">=</span> <span class="p">[</span><span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">]</span>
<span class="n">threads</span> <span class="o">=</span> <span class="mi">50</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="mi">10000</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="nb">puts</span> <span class="n">flags</span><span class="p">.</span><span class="nf">to_s</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">flags</span><span class="p">.</span><span class="nf">map!</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="o">!</span><span class="n">f</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">threads</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
</code></pre>
<p>执行以上代码,你会得到类似下面的结果:</p>
<pre class="highlight"><code>$ ruby visibility.rb > visibility.log
$ grep -Hnri 'true, false' visibility.log | wc -l
3151
$ _
</code></pre>
<p>这表示有线程读取到了其它线程未完全同步到内存的数据。想要避免这个问题,只需要把读取 <code>flags</code> 的代码也放在 <code>mutex.synchronize</code> 的代码块内即可:</p>
<pre class="highlight"><code class="language-ruby"><span class="o">...</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="nb">puts</span> <span class="n">flags</span><span class="p">.</span><span class="nf">to_s</span>
<span class="k">end</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">flags</span><span class="p">.</span><span class="nf">map!</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="o">!</span><span class="n">f</span> <span class="p">}</span>
<span class="k">end</span>
<span class="o">...</span>
</code></pre>
<h2>
<a id="Mutex+%E7%9A%84%E6%80%A7%E8%83%BD" href="#Mutex+%E7%9A%84%E6%80%A7%E8%83%BD" class="anchor"></a>Mutex 的性能</h2>
<p>Mutex 在关键的地方保护着数据在同一时刻只能被一个线程修改,其实 GIL 就是一个 mutex.</p>
<p>但像上面这样一来,并行变成串行,执行速度就明显更慢了。因此,持有 mutex 的代码块应该尽可能的小。因为只需要在取值的时候持有 mutex, 可以把 <code>puts</code> 这样可以并行执行的阻塞 IO 操作移出来,上面的代码可以优化如下:</p>
<pre class="highlight"><code class="language-ruby"><span class="o">...</span>
<span class="n">flags_string</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">flags_string</span> <span class="n">flags</span><span class="p">.</span><span class="nf">to_s</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="n">flags_string</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">flags</span><span class="p">.</span><span class="nf">map!</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="o">!</span><span class="n">f</span> <span class="p">}</span>
<span class="k">end</span>
<span class="o">...</span>
</code></pre>
<p>在我的 x220 笔记本上,这段代码的执行时间由 20 秒减少到了 10 秒,直接少了一半。要是这里用的不是 <code>puts</code> 而是发起一个网络请求,那么这个优化的收益会更可观——或者换句话说,未优化的代码的执行速度会极度地慢,下面就是一个例子:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'thread'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="n">mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get_response</span><span class="p">(</span><span class="s1">'dynamic.xkcd.com'</span><span class="p">,</span> <span class="s1">'/random/comic/'</span><span class="p">)</span>
<span class="n">random_comic_url</span> <span class="o">=</span> <span class="n">response</span><span class="p">[</span><span class="s1">'Location'</span><span class="p">]</span>
<span class="vi">@results</span> <span class="o"><<</span> <span class="n">random_comic_url</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
<span class="nb">puts</span> <span class="vi">@results</span>
</code></pre>
<p>如果把 HTTP 请求移出 <code>synchronize</code> 块,就能极大地提升性能。另外如果这里的 <code>@results</code> 用的是 <code>Queue</code> 而不是 <code>Array</code>, 就没有 mutex 什么事了。因为 <code>Queue</code> 本身是线程安全的。</p>
<h2>
<a id="%E6%AD%BB%E9%94%81" href="#%E6%AD%BB%E9%94%81" class="anchor"></a>死锁</h2>
<p>线程 A 在执行代码时持有了 mutex_a, 线程 B 在执行代码时持有了 mutex_b. 这时,线程 A 需要访问线程 B 正在访问的资源,尝试去获取 mutex_b, 而线程 B 也需要访问线程 A 正在访问的资源,尝试去获取 mutex_a. 此时因为线程 A 没有执行完毕,mutex_a 就没有释放;同样因为线程 B 也没有执行完毕,mutex_b 也就没有释放。线程 A 和线程 B 都在等待对方的 mutex 释放,这种状态就被称为死锁。</p>
<p><img src="/attachments/mxvwTKX8apEVnuSt4Bsuqxqk/deadlock.png" alt="死锁"></p>
<p>由此可以知道,死锁通常发生在线程需要获取多个 mutex 的时候。</p>
<p>Ruby 的 <code>Mutex</code> 提供了一个 <code>try_lock</code> 的方法,该方法的用法跟 <code>lock</code> 一样,区别在于 <code>try_lock</code> 在 mutex 不可用时不会一直等待,而是返回 false. 如果获取到 mutex, 则返回 true.</p>
<p>但如果只是在 <code>try_lock</code> 返回 false 后不停重试,并不解决问题,程序只是由死锁状态变成了死循环。正确的做法是在 <code>try_lock</code> 失败之后,线程各自释放掉自己的 mutex 重新从头开始执行,期待通过多线程本身的不确定性,使之在重新开始后能够执行成功。</p>
<p>即便如此,过程中也可能有不确定次数的重试,而且似乎很多时候,代码是不可以重复执行的,否则会产生预期以外的数据(这么说这个 <code>try_lock</code> 方案根本不解决问题)。</p>
<p>一个更好的方案是定义一个 mutex 层级,或者换句话说:当两个线程都需要获取多个 mutex 的时候,确保它们获取这些 mutex 的顺序是一致的。这样便可以在任何时候避免死锁。</p>
<h1>
<a id="%E9%80%9A%E8%BF%87%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F%E7%BB%99%E7%BA%BF%E7%A8%8B%E5%8F%91%E4%BF%A1%E5%8F%B7" href="#%E9%80%9A%E8%BF%87%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F%E7%BB%99%E7%BA%BF%E7%A8%8B%E5%8F%91%E4%BF%A1%E5%8F%B7" class="anchor"></a>通过条件变量给线程发信号</h1>
<p>条件变量 (Condition Variable) 可以用于线程间发送事件通知。<code>ConditionVariable</code> 的 <code>wait(mutex)</code> 方法会让当前线程释放 mutex 并进入睡眠状态,<code>signal()</code> 方法可以唤醒在 <code>ConditionVariable</code> 实例上等待的<strong>一个</strong>线程:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'thread'</span>
<span class="nb">require</span> <span class="s1">'net/http'</span>
<span class="n">mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="n">condvar</span> <span class="o">=</span> <span class="no">ConditionVariable</span><span class="p">.</span><span class="nf">new</span>
<span class="n">results</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s1">'https://dynamic.xkcd.com/random/comic/'</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">get_response</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
<span class="n">random_comic_url</span> <span class="o">=</span> <span class="n">response</span><span class="p">[</span><span class="s1">'Location'</span><span class="p">]</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="k">if</span> <span class="n">random_comic_url</span> <span class="o">&&</span> <span class="o">!</span><span class="n">random_comic_url</span><span class="p">.</span><span class="nf">empty?</span>
<span class="n">results</span> <span class="o"><<</span> <span class="n">random_comic_url</span>
<span class="n">condvar</span><span class="p">.</span><span class="nf">signal</span> <span class="c1"># Signal the ConditionVariable</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="k">while</span> <span class="n">results</span><span class="p">.</span><span class="nf">empty?</span>
<span class="n">condvar</span><span class="p">.</span><span class="nf">wait</span><span class="p">(</span><span class="n">mutex</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">results</span><span class="p">.</span><span class="nf">shift</span>
<span class="nb">puts</span> <span class="s2">"You should check out </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:join</span><span class="p">)</span>
</code></pre>
<p>这段代码中比较让人疑惑的地方在于 <code>while results.empty?</code>, 似乎这里使用 <code>if results.empty?</code> 就可以了。但实际上 <code>ConditionVariable#signal</code> 只是发送事件通知,告诉等待着的线程“有另一个线程往 <code>results</code> 里塞了一条数据”,并不保证其它事情——比如说“这条数据会一直等着你去取”,很可能该数据会被其它线程取走。</p>
<p><code>ConditionVariable</code> 还有一个 <code>broadcast()</code> 方法,用于唤醒在当前 <code>ConditionVariable</code> 实例上等待的所有线程:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="k">if</span> <span class="n">random_comic_url</span> <span class="o">&&</span> <span class="o">!</span><span class="n">random_comic_url</span><span class="p">.</span><span class="nf">empty?</span>
<span class="n">results</span> <span class="o"><<</span> <span class="n">random_comic_url</span>
<span class="n">condvar</span><span class="p">.</span><span class="nf">broadcast</span> <span class="k">if</span> <span class="n">results</span><span class="p">.</span><span class="nf">size</span> <span class="o">==</span> <span class="mi">10</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>下面是结合使用 <code>Mutex</code> 和 <code>ConditionVariable</code> 实现的一个线程安全的 <code>BlockingQueue</code>:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'thread'</span>
<span class="k">class</span> <span class="nc">BlockingQueue</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@storage</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@mutex</span> <span class="o">=</span> <span class="no">Mutex</span><span class="p">.</span><span class="nf">new</span>
<span class="vi">@condvar</span> <span class="o">=</span> <span class="no">ConditionVariable</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">push</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
<span class="vi">@mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="vi">@storage</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
<span class="vi">@condvar</span><span class="p">.</span><span class="nf">signal</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">pop</span>
<span class="vi">@mutex</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
<span class="k">while</span> <span class="vi">@storage</span><span class="p">.</span><span class="nf">empty?</span>
<span class="vi">@condvar</span><span class="p">.</span><span class="nf">wait</span><span class="p">(</span><span class="vi">@mutex</span><span class="p">)</span>
<span class="k">end</span>
<span class="vi">@storage</span><span class="p">.</span><span class="nf">shift</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>前面已经提到过,实际上 Ruby 自带了一个 <code>Queue</code> 类型,这是 Ruby 中唯一一个线程安全的数据结构类型。 <code>require 'thread'</code> 之后便可以使用。其它数据结构包括 <code>Array</code>, <code>Hash</code> 等,都不是线程安全的,即使在 JRuby 的实现当中也一样。因为线程安全相关的代码会降低其在单线程程序中的性能。如果非要用线程安全的数据结构不可,可以考虑使用 <a href="http://github.com/headius/thread_safe">thread_safe</a> 这个 Gem.</p>
本文是学习笔记,学习过程中主要阅读和参考了以下资料,记录的代码片断也来自以下链接。部分代码稍作了修改。最后那个链接虽然有些标题党,但是内容很值得一看:
Working with Ruby Th...
yuan
https://geeknote.net/yuan