<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Все посты</title>
    <description>Посты sashaqred.com</description>
    <language>ru</language>
    <link >https://sashaqred.com/ru/</link>

    <atom:link href="https://sashaqred.com/ru/feed.xml" rel="self" type="application/rss+xml"/>
      <item>
        <title>Порядок операторов в RxJs</title>
        <link>https://sashaqred.com/ru/blog/rxjs-operators-order/</link>
        <description>
          <![CDATA[
            <p>TL;DR: Порядок важен. Операторы довольно атомарны и зачастую очень просты, но это не мешает им объединяться в сложные последовательности, в которых легко допустить ошибку. Давайте разберемся.</p>
            <h2 id="prostaya-posledovatelnost" tabindex="-1">Простая последовательность</h2>
<p>Начнем с обычного интервала.</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Его диаграмма будет такой:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval(1000)</span>
-----0-----1-----2-----3-----4---></code></pre>
<p>Интервал выводит числа по возрастанию, давайте прокачаем его и добавим немного случайности с помощью <code class="language-">Math.random()</code>:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Для упрощения диаграммы, будем считать, что <code class="language-">Math.random()</code> возвращает целые числа.</p>
<p>Обновим диаграмму:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval(1000)</span>
-----0-----1-----2-----3-----4--->
<span class="token comment">// map(() => Math.random())</span>
-----1-----6-----3-----2-----9---></code></pre>
<p>Ждать первое значение целую секунду — это долго. Хочется сразу после подписки видеть значение в консоли. Есть прекрасный оператор <code class="language-">startWith</code>. Добавим его в нашу цепочку операторов:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">startWith</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Можете предположить результат?</p>
<p>Помните, я говорил, что порядок важен. Это как раз хороший пример для данного утверждения. Давайте посмотрим на диаграмму:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval(1000)</span>
-----0-----1-----2-----3-----4--->
<span class="token comment">// map(() => Math.random())</span>
-----1-----6-----3-----2-----9--->
<span class="token comment">// startWith(-1)</span>
(-1)-1-----6-----3-----2-----9--->
↑    ↑</code></pre>
<p><em>Упс</em>. Значение добавляется <em>после</em> нашей функции случайности. Название немного сбивает, и, мне кажется, что используя <code class="language-">startWith</code> в первый раз, я поставил этот оператор не туда.</p>
<p>Как это работает: под капотом оператор <a href="https://github.com/ReactiveX/rxjs/blob/f57e1fc287590b6e20ffe2d6b1b9685068b21cdf/src/internal/Observable.ts#L66">создаёт новый стрим</a>, который передается в следующий оператор. Поэтому получается, что <code class="language-">startWith</code> принимает стрим, который пришел из <code class="language-">map</code>, и уже в этот стрим записывается первое значение.</p>
<p>Окей, теперь зная это, поправим код так, чтобы все цифры проходили через <code class="language-">map</code>, в том числе и результат <code class="language-">startWith</code>.</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">startWith</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Получим такую диаграмму:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval(1000)</span>
-----0-----1-----2-----3-----4--->
<span class="token comment">// startWith(-1)</span>
(-1)-0-----1-----2-----3-----4--->
↑    ↑
<span class="token comment">// map(() => Math.random())</span>
2----1-----6-----3-----2-----9---></code></pre>
<p>Перфекто.</p>
<p>У меня есть к вам вопрос: что будет в консоли (или как будет выглядеть диаграмма) при такой последовательности операторов?</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">startWith</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">startWith</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span>
  <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Операторы же выполняются по порядку. Ведь так? Ведь так!?</p>
<p>Да, все так. Только надо внимательно следить за происходящим и помнить: каждый оператор создает <em>новый</em> поток. Разберем по шагам:</p>
<ol>
<li>Создаем поток из <code class="language-">interval(1000)</code>;</li>
<li>К этому потоку <code class="language-">startWith</code> добавляет в самое начало <code class="language-">-1</code>;</li>
<li>Выполняем <code class="language-">Math.random()</code>;</li>
<li>К потоку из <em>предыдущего</em> шага следующий <code class="language-">startWith</code> в самое начало добавляет <code class="language-">'a'</code>;</li>
<li>Сразу после подписки мы увидим <code class="language-">'a'</code>, следом за ним будет результат <code class="language-">Math.random()</code>, который выполнился из-за <code class="language-">-1</code>. Все это будет происходить синхронно.</li>
<li>Остальные значения будут выводиться асинхронно в консоли раз в секунду.</li>
</ol>
<p>Диаграмма все упростит (<em>надеюсь</em>):</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval(1000)</span>
-----0-----1-----2-----3-----4--->
<span class="token comment">// startWith(-1)</span>
(-1)-0-----1-----2-----3-----4--->
↑    ↑
<span class="token comment">// map(() => Math.random())</span>
2----1-----6-----3-----2-----9--->
<span class="token comment">// startWith('a')</span>
a2---1-----6-----3-----2-----9--->
⇈</code></pre>
<p>Порядок операторов важен. Но иногда бывает не так очевидно, что придет в <code class="language-">subscribe</code>, в какой последовательности и почему.</p>
<h2 id="share-replay" tabindex="-1">shareReplay</h2>
<p>Есть операторы (или группы операторов), которые требуют к себе особого внимания: их неправильное использование может привести к утечкам памяти.</p>
<p>Начнем с группы операторов шаринга состояния. Все примеры будут с оператором <code class="language-">shareReplay</code>, но это применимо и к другим операторам и их комбинациям из <a href="https://reactive.how/rxjs/sharing">«sharing группы»</a>.</p>
<p>Мой общий совет звучит так: <code class="language-">shareReplay</code> должен быть последним оператором в <code class="language-">.pipe()</code>. Если вы располагаете его в другом месте, либо вы знаете, зачем он там нужен, либо совершаете ошибку. Разберемся почему.</p>
<p>Взглянем на <code class="language-">shareReplay</code>, точнее, на его отсутствие:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Каждый <code class="language-">subscribe</code> создает свой поток интервалов, который потом преобразуется с помощью <code class="language-">Math.random()</code>. Получается, что каждую секунду мы будем видеть 2 цифры в консоли, но они будут разные.</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// subscribe</span>
-----3-----7-----1-----8-----9--->
<span class="token comment">// subscribe</span>
-----5-----2-----1-----7-----0---></code></pre>
<p>Если мы сделаем подписку на один из потоков асинхронной:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>то каждая подписка будет писать в консоль число независимо от предыдущей. Будет такая диаграмма результата:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// subscribe</span>
-----3-----7-----1-----8-----9--->
<span class="token comment">// setTimeout</span>
   ----5-----2-----1-----7-----0-></code></pre>
<p><em>Обратите внимание, что вторая подписка начинается не сразу.</em></p>
<p>Если мы хотим во всех подписках использовать один и тот же интервал, а не создавать его каждый раз заново, то без <code class="language-">shareReplay</code> не обойтись:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
  <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Угадаете, какой будет диаграмма?</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// subscribe</span>
-----3-----7-----1-----8-----9--->
<span class="token comment">// setTimeout</span>
   --5-----2-----1-----7-----0---></code></pre>
<p>Да, мы подписываемся с задержкой в 500мс. Но так как мы шарим интервал, задержка не важна: значения будут отображаться в консоли одновременно. Давайте поменяем интервал на 1500мс:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
  <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1500</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Диаграмма:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// subscribe</span>
-----3-----7-----1-----8-----9--->
<span class="token comment">// setTimeout</span>
        5--2-----1-----7-----0---></code></pre>
<p><code class="language-">shareReplay</code> работает таким образом, что запоминает последнее значение, и, если оно было, мы получаем его мгновенно, без задержек. А все последующие значения будут отображаться во время срабатывания таймера.</p>
<p>Идем дальше. А что если нам надо шарить результат и <code class="language-">Math.random()</code> в том числе? Надо поместить <code class="language-">shareReplay</code> чуть-чуть подальше:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
  <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token builtin">console</span><span class="token punctuation">.</span>log<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1500</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Думаю, что диаграмма окажется вполне очевидной:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// subscribe</span>
-----3-----7-----1-----8-----9--->
<span class="token comment">// setTimeout</span>
        3--7-----1-----8-----9---></code></pre>
<p>В наших примерах, когда <code class="language-">shareReplay</code> находился перед <code class="language-">map</code>, это был баг, а не фича. Как я упоминал ранее, операторы, подобные <code class="language-">shareReplay</code>, в большинстве случаев надо использовать в конце пайпа. Я обычно добавляю этот оператор в конце <strong>каждого</strong> пайпа.</p>
<h2 id="take-until-i-avtomaticheskaya-otpiska" tabindex="-1">takeUntil и автоматическая отписка</h2>
<p><em>* Сейчас я буду говорить про один из классических для Angular подходов автоматической отписки. Если вы используете RxJs в хуках React'а и/или используете другой подход автоматической отписки, то это правило именно в такой формулировке к вам не применимо. Об использовании <code class="language-">takeUntil</code> по другому поговорим чуть позже. На этом подходе можно хорошо показать важность правильного расположения операторов друг между другом.</em></p>
<p>Перейдем ко второй группе: операторы завершения потока. Примеры будут с <code class="language-">takeUntil</code>, но это применимо также и к <a href="https://reactive.how/rxjs/taking">другим операторам аналогичной группы</a>.</p>
<p>К <code class="language-">takeUntil</code> в рамках Angular я тоже применяю одно общее правило использования: <code class="language-">takeUntil</code> должен быть последним оператором перед <code class="language-">subscribe</code> и должен быть всегда.</p>
<p>Почему так? Разберем на примере одного из типовых применений <code class="language-">takeUntil</code> в Angular-компонентах, но с неправильным порядком операторов.</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">class</span> <span class="token class-name">MyComponent</span> <span class="token punctuation">{</span>
  <span class="token keyword">private</span> destroy$ <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReplaySubject</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
        <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>apiService<span class="token punctuation">.</span><span class="token function">ping</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token function">ngOnDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">.</span><span class="token function">complete</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Что тут произойдет? Если в момент того, как срабатывает <code class="language-">destroy$</code>, запрос находится ещё в процессе, то мы его не отменим. И, кстати, мы не можем гарантировать, что поток возвращаемый методом <code class="language-">ping()</code>, когда-нибудь завершится. А это уже выглядит как утечка памяти.</p>
<p>Надо сделать правильный порядок вещей:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
      <span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>apiService<span class="token punctuation">.</span><span class="token function">ping</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Теперь никаких утечек😃.</p>
<p>Я упомянул, что <code class="language-">takeUntil</code> для автоматической отписки надо писать прямо перед <code class="language-">subscribe</code>, но почему не в конце пайпа, как мы делали с <code class="language-">shareReplay</code>? Например, вот так:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Если мы к нашему <code class="language-">randomTimer</code> перед подпиской добавим что-нибудь еще, например, знакомый нам <code class="language-">switchMap</code>:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">;</span>

  randomTimer<span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>apiService<span class="token punctuation">.</span><span class="token function">ping</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Мы сможем снова сказать «привет 👋» утечке памяти. Мы не можем контролировать то, <em>как</em> будет использоваться наш поток в дальнейшем, поэтому механизм автоматической подписки надо реализовывать не во всех пайпах, а только перед вызовом <code class="language-">subscribe</code>. Давайте исправим утечку:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">;</span>

  randomTimer<span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>apiService<span class="token punctuation">.</span><span class="token function">ping</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Не стоит забывать отписываться от подписок. Даже если вы подписываетесь на один из методов <code class="language-">HttpClient</code>. Даже если вы это делаете в сервисе, который <code class="language-">providedIn: 'root'</code>. <em>Отписывайтесь. Всегда.</em></p>
<h2 id="bolshe-take-until" tabindex="-1">Больше takeUntil</h2>
<p><code class="language-">takeUntil</code> можно использовать не только для автоматических отписок. Расширим наш компонент, который делал пинг:</p>
<ul>
<li>Пинг происходит только тогда, когда курсор находится вне компонента.</li>
<li>Как только курсор перемещается на компонент, мы перестаем делать пинг.</li>
<li>Также не стоит забывать про автоматическую отписку, мы же не хотим утечек памяти.</li>
</ul>
<p>Пропустим стадию гугления различных операторов, код я уже написал:</p>
<pre class="language-ts"><code class="language-ts"><span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
    <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>mouseIn$<span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">repeatWhen</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>mouseOut$<span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">switchMap</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>apiService<span class="token punctuation">.</span><span class="token function">ping</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">takeUntil</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>destroy$<span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>Пффф... Осталось понять, что тут происходит. Начнем с диаграммы:</p>
<pre class="language-asciidoc"><code class="language-asciidoc"><span class="token comment">// interval</span>
-----0-----1--|  -----0---|
<span class="token comment">// takeUntil(this.mouseIn$)</span>
--------------t-----------|
<span class="token comment">// repeatWhen</span>
-----------------r--------|
<span class="token comment">// switchMap</span>
-----<span class="token inline"><span class="token punctuation">+</span>-----<span class="token punctuation">+</span></span>----------+---|
      \     \          \
       --s|  --s|       --|
<span class="token comment">// takeUntil(this.destroy$)</span>
-------------------------d|
<span class="token comment">// subscribe</span>
---------e-----e----------|</code></pre>
<p>Ох... Думаю, что легче не стало. Немного текстового пояснения. Сначала мы подписываемся на <code class="language-">interval</code>. Каждый раз когда <code class="language-">interval</code> эммитит значение, наш <code class="language-">switchMap</code> делает пинг. Как только пинг эммитит значение, мы получаем значение в <code class="language-">subscribe</code>. Пока все просто. Как только <code class="language-">this.mouseIn$</code> заэммитит событие, мы отписываемся <strong>только</strong> от <code class="language-">interval</code>. Обратите внимание, что <code class="language-">switchMap</code> <strong>не</strong> отменил работу пинга, мы получили его результат после отписки от интервала. После этого как только <code class="language-">this.mouseOut$</code> получает событие, мы <strong>заново</strong> подписываемся на <code class="language-">interval</code>. И начинаем всю нашу цепочку заново. Стоит только <code class="language-">this.destroy$</code> получить событие, как мы сразу отписываемся от всех предшествующих потоков, и, получается, что мы отписываемся от <strong>всего</strong>.</p>
<p>С помощью простых комбинаций операторов можно реализовывать довольно сложную логику.</p>
<h2 id="mozhno-li-slomat-poryadok" tabindex="-1">Можно ли сломать порядок?</h2>
<p>А что на счет операторов, которые срабатывают в определенный момент? Например <code class="language-">finalize</code>.</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
  <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">finalize</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'finished'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Мы увидим в консоли <code class="language-">'finished'</code>, как только от подписки отпишутся. Звучит просто. А если вот так?</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> randomTimer <span class="token operator">=</span> <span class="token function">interval</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>
  <span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">finalize</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'finished 1'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">shareReplay</span><span class="token punctuation">(</span><span class="token punctuation">{</span> refCount<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> bufferSize<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
  <span class="token function">finalize</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'finished 2'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">const</span> a <span class="token operator">=</span> randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> b <span class="token operator">=</span> randomTimer<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

a<span class="token punctuation">.</span><span class="token function">unsubscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Какие предположения? Увидим ли мы <code class="language-">'finished 1'</code>? А что насчет <code class="language-">'finished 2'</code>?</p>
<p>Результатом выполнения этого примера в консоли будет только <code class="language-">'finished 2'</code>.</p>
<p>Чтобы увидеть <code class="language-">'finished 1'</code>, надо сделать <code class="language-">b.unsubscribe()</code>. Когда мы это сделаем, сначала появится <code class="language-">'finished 1'</code>, а затем — <code class="language-">'finished 2'</code>. Ведь ... да, операторы выполняются последовательно.</p>
<p>Почему так происходит? Помните, как работают операторы? Каждый оператор создает новый поток. А еще <code class="language-">shareReplay</code>, который помогает не создавать каждый раз кучу новых потоков, а переиспользовать один текущий. <code class="language-">shareReplay({refCount: true, bufferSize: 1})</code> живет, пока существует хотя бы одна подписка, поэтому первый <code class="language-">finalize</code> срабатывает не сразу. Но действие <code class="language-">shareReplay</code> не распространяется на операторы, которые идут после него. Поэтому каждый <code class="language-">unsubscribe</code> будет триггерить второй <code class="language-">finalize</code>.</p>
<p>Вот еще несколько операторов, которые выполняются «в определенный момент»: <code class="language-">defaultIfEmpty</code>, <code class="language-">throwIfEmpty</code>, <code class="language-">retryWhen</code>, <code class="language-">catchError</code>, <code class="language-">repeatWhen</code>.</p>
<p>Эти операторы не выполняются в общем потоке обычных операторов, но когда приходит их время, они будут следовать заданной последовательности.</p>
<h2 id="vmesto-vyvoda-validiruem-poryadok" tabindex="-1">Вместо вывода: валидируем порядок</h2>
<p>За порядком операторов следить не просто. <code class="language-">shareReplay</code> расположили не в том месте — получили повторяющиеся вычисления. Забыли <code class="language-">takeUntil</code>— получили утечку памяти. В NgRx эффекте поставили <code class="language-">first</code> после <code class="language-">switchMap</code>, а не внутри, — сломали повторное выполнение экшенов. В экшене на удаление использовали <code class="language-">switchMap</code>, а не <code class="language-">mergeMap</code>, — сломали последовательное удаление множества сущностей.</p>
<p>В голове держать все эти нюансы не просто. К счастью, часть этой ментальной нагрузки можно переложить на процессор. Главное правильно настроить линтер. И в этом нам помогут пакеты <a href="https://github.com/cartant/eslint-plugin-rxjs">eslint-plugin-rxjs</a> и <a href="https://github.com/cartant/eslint-plugin-rxjs-angular">eslint-plugin-rxjs-angular</a>.</p>
<p>В <code class="language-">eslint-plugin-rxjs</code> советую включить как минимум рекомендуемые настройки <code class="language-">plugin:rxjs/recommended</code>.</p>
<p>Если вы используете какой-нибудь стор, который построен на RxJs, типа NgRx, то обратите внимание на эти правила: <code class="language-">rxjs/no-cyclic-action</code>, <code class="language-">rxjs/no-unsafe-catch</code>, <code class="language-">rxjs/no-unsafe-first</code>, <code class="language-">rxjs/no-unsafe-switchmap</code>. Это поможет не допускать простых ошибок в эффектах.</p>
<p>А для Angular надо включить то правило, которое соответствует вашей идеологии по автоматическим отпискам. Я предпочитаю <code class="language-">rxjs-angular/prefer-takeuntil</code>.</p>
<p>Если вы строите свое приложение с помощью NgRx, вам поможет еще один плагин <a href="https://github.com/timdeschryver/eslint-plugin-ngrx">eslint-plugin-rxjs-ngrx</a>. В нем уже есть готовые конфигурации, советую включить <code class="language-">plugin:ngrx/recommended</code> или <code class="language-">plugin:ngrx/strict</code>, это поможет избегать типовых ошибок при использовании NgRx.</p>

          ]]>
        </description>
        <pubDate>Mon, 27 Dec 2021 00:00:00 GMT</pubDate>
        <guid>https://sashaqred.com/ru/blog/rxjs-operators-order/</guid>
      </item>

  </channel>
</rss>
