让我们一起来学习 RxJS

aezi3943 8年前
   <h2><strong>What Is RxJS</strong></h2>    <p>通过 <strong> 阅读 <a href="/misc/goto?guid=4959717416649186477" rel="nofollow,noindex">官方文档</a> </strong> ,不难得出:RxJS 可以很好 <strong>解决异步和事件组合的问题</strong> 。</p>    <p>这个时候我就有疑问了,异步问题不是用 Promise ( async/await ) 就好了吗?</p>    <p>至于事件,配合框架 ( React, Vue, Angular2 等 ) 的话不也很容易解决吗?</p>    <p>不管怎样, 让我们先看个 Hello World 吧。( 我要看 <a href="/misc/goto?guid=4959717416732300904" rel="nofollow,noindex">DEMO</a> )</p>    <h3><strong>Rx's Hello World</strong></h3>    <pre>  <code class="language-javascript">// auto-complete  const Observable = Rx.Observable    const input = document.querySelector('input')    const search$ = Observable.fromEvent(input, 'input')      .map(e => e.target.value)    .filter(value => value.length >= 1)    .throttleTime(100)    .distinctUntilChanged()    .switchMap(term => Observable.fromPromise(wikiIt(term)))    .subscribe(      x => renderSearchResult(x),      err => console.error(err)    )</code></pre>    <p>上面的代码做了以下事情:</p>    <ul>     <li> <p>监听input元素的input事件</p> </li>     <li> <p>一旦发生,把事件对象e映射成input元素的值</p> </li>     <li> <p>接着过滤掉值长度小于1的</p> </li>     <li> <p>并且还设置了一个throttle( 节流器 ),两次输入间隔不超过 100 毫秒为有效输入</p> </li>     <li> <p>如果该值和过去最新的值相等的话,忽略他</p> </li>     <li> <p>最后,拿到值便调用Wikipedia的一个API</p> </li>     <li> <p>最后的最后,需要subscribe才能拿到API返回的数据</p> </li>    </ul>    <p>是不是看起来就觉得很cool,好想学!</p>    <p>短短几行代码就完成了一个 auto-complete 组件。</p>    <h2><strong>How It Works</strong></h2>    <p>那上面的代码是什么意思?</p>    <p>RxJS 到底是如何工作的?如何解决异步组合问题的?</p>    <h3><strong>Observable</strong></h3>    <p>Rx 提供了一种叫 <strong>Observable</strong> 的数据类型,兼容 ECMAScript 的 <a href="/misc/goto?guid=4959717416820674705" rel="nofollow,noindex">Observable Spec Proposal</a> 草案标准。他是 Rx 最核心的数据类型,结合了 <a href="/misc/goto?guid=4959617323358662558" rel="nofollow,noindex">Observer Pattern</a> , <a href="/misc/goto?guid=4959717416925455069" rel="nofollow,noindex">Iterator Pattern</a> 。</p>    <p>那到底什么是 Observable ?</p>    <p>Observable 其实就是一个 <strong>异步的数组</strong> 。 <em> ( ---> <a href="/misc/goto?guid=4959717417011932155" rel="nofollow,noindex">2 minute introduction to rx</a> ) </em></p>    <p>不妨想像一下, <strong>数组 + 时间轴 = Observable</strong> 。</p>    <p>数组元素的值是未来某个时间点 <em>emit</em> ( 产生 ) 的,但是我们并不关心这个时间点,因为利用了「观察者模式」 <em>subscribe</em> ( 订阅 ) 了这个数组,只要他 <em>emit</em> 了值,就会自动 <em>push</em> 给我们。</p>    <p>我们再用图来表示一下的话:</p>    <pre>  <code class="language-javascript">--a---b-c--d-----e--|--></code></pre>    <p>这种图叫做 <a href="/misc/goto?guid=4959717417097542244" rel="nofollow,noindex">marble diagram</a> 。</p>    <p>我们可以把 ASCII 的 marble 图转成 SVG 的: <a href="/misc/goto?guid=4959717417176323661" rel="nofollow,noindex">ASCII -> SVG</a> 。</p>    <p>- 表示时间轴, a ~ e 表示 emit 的值, | 则表示这个 stream 已经结束了。</p>    <p>比方说, click 事件用上图来表示: a 表示第 1 次点击, b 表示第 2 次点击,如此类推。</p>    <p>如果你觉得 Observable 这个名字不够形象不够 cool 的话,你可把他叫做 <a href="/misc/goto?guid=4959717417265409317" rel="nofollow,noindex">stream</a> ,因为他的 marble 图就像 steam 一样。所以啊,下面我都会把 <em>Observable</em> 称作 <em>stream</em> 。</p>    <h3><strong>Operators</strong></h3>    <p>那么,我们怎么对 stream 进行操作呢?怎么把多个 stream 组合在一起呢?</p>    <p>我们前面不是说了「 Observable 其实就是 <em>异步数组</em> 」吗?在 JavaScript 里的数组不是有很多内置的方法吗?比如 map , filter , reduce 等等。类似地,Observable 也有自己的方法,也就是所谓的 <a href="/misc/goto?guid=4959717417356272759" rel="nofollow,noindex">operator</a> 。比如上面例子中的 map , filter , throttleTime , distinctUntilChanged 等等很多很有用的 operator 。</p>    <p>面对 RxJS 那么多 operator ,我们要怎么学习呢?很简单:</p>    <p><a href="/misc/goto?guid=4959717417436577163" rel="nofollow,noindex">分类别</a> + <a href="/misc/goto?guid=4959717417097542244" rel="nofollow,noindex">画 marble 图</a> + <a href="/misc/goto?guid=4959717417533214332" rel="nofollow,noindex">看例子</a> + <a href="/misc/goto?guid=4959717417620401627" rel="nofollow,noindex">选</a></p>    <p>现在,就让我们画出上面 Hello World 例子的 marble 图。</p>    <pre>  <code class="language-javascript">const search$ = Observable.fromEvent(input, 'input')      .map(e => e.target.value)    .filter(value => value.length >= 1)    .throttleTime(100)    .distinctUntilChanged()    .switchMap(term => Observable.fromPromise(wikiIt(term)))    .subscribe(      x => renderSearchResult(x),      err => console.error(err)    )</code></pre>    <p>假设输入了 5 次,每次输入的值一次为: a , ab , c , d , c ,并且第 3 次输入的 c 和第 4 次的 d 的时间间隔少于 100ms :</p>    <pre>  <code class="language-javascript">---i--i---i-i-----i---|--> (input)          map    ---a--a---c-d-----c---|-->        b          filter    ---a--a---c-d-----c---|-->        b        throttleTime    ---a--a---c-------c---|-->        b    distinctUntilChanged    ---a--a---c----------|-->        b      switchMap    ---x--y---z----------|--></code></pre>    <p>如果我告诉你学习 <strong>RxJS 的捷径是「学会看和画 marble 图」</strong> ,你信还是不信?</p>    <h2><strong>Learn By Doing</strong></h2>    <p>现在,就让我们结合上面的知识,来实现一个简单的 canvas 画板。</p>    <p>根据 canvas 的 <a href="/misc/goto?guid=4959717417696380732" rel="nofollow,noindex">API</a> ,我们需要知道两个点的坐标,这样才能画出一条线。</p>    <h3>Step 1</h3>    <p>( 我要看 <a href="/misc/goto?guid=4959717417791408077" rel="nofollow,noindex">DEMO</a> )</p>    <p>那么,现在我们需要做的是 <strong>创建</strong> 一个关于鼠标移动的 stream 。于是,我们 <strong>去文档找对应的 operator 类别</strong> ,也就是 <a href="/misc/goto?guid=4959717417889026926" rel="nofollow,noindex">Creation Operators</a> ,然后得到 <a href="/misc/goto?guid=4959717417978417214" rel="nofollow,noindex">fromEvent</a> 。</p>    <pre>  <code class="language-javascript">const canvas = document.querySelector('canvas')    const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')</code></pre>    <p>对应的 marble 图:</p>    <pre>  <code class="language-javascript">--m1---m1-m2--m3----m4---|-->  (mousemove)</code></pre>    <p>接着,我们需要拿到每次鼠标移动时的坐标。也就是说:需要 <strong>变换</strong> stream 。</p>    <p>对应类别的 operator 文档: <a href="/misc/goto?guid=4959717418058391866" rel="nofollow,noindex">Transformation Operators</a> ---> <a href="/misc/goto?guid=4959717418145765344" rel="nofollow,noindex">map</a> 。</p>    <pre>  <code class="language-javascript">const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')      .map(e => ({ x: e.offsetX, y: e.offsetX }))</code></pre>    <p>此时的 marble 图:</p>    <pre>  <code class="language-javascript">--m1---m2-m3--m4----m5---|-->  (mousemove)          map  --x1---x2-x3--x4----x5---|-->  (点坐标)</code></pre>    <p>然后,怎么拿到两个点的坐标呢?我们需要再 <strong>变换</strong> 一下 stream 。</p>    <p>对应类别的 operator 文档: <a href="/misc/goto?guid=4959717418058391866" rel="nofollow,noindex">Transformation Operators</a> ---> <a href="/misc/goto?guid=4959717418238229331" rel="nofollow,noindex">bufferCount</a> 。</p>    <pre>  <code class="language-javascript">const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')      .map(e => ({ x: e.offsetX, y: e.offsetY }))    .bufferCount(2)</code></pre>    <p>marble 图:</p>    <pre>  <code class="language-javascript">--m1---m2-m3--m4----m5---|-->  (mousemove)          map    --x1---x2-x3--x4----x5---|-->  (点坐标)        bufferCount(2)    -------x1-----x3----x5---|---> (两点坐标)         x2     x4</code></pre>    <p>然而你会发现,此时画出来的 <a href="/misc/goto?guid=4959717417791408077" rel="nofollow,noindex">线段是不连续的</a> 。为什么?我也不知道!!</p>    <p>那就让我们看看别人是怎么写的吧: <a href="/misc/goto?guid=4959717418329214113" rel="nofollow,noindex">canvas paint</a> 。</p>    <h3><strong>Step 2</strong></h3>    <p>( 先让我要看看 <a href="/misc/goto?guid=4959717418408268534" rel="nofollow,noindex">DEMO</a> )</p>    <p>换了一种思路,并没有 <strong>变换</strong> stream ,而是把两个 stream <strong>组合</strong> 在一起。</p>    <p>查看文档 <a href="/misc/goto?guid=4959717418492069198" rel="nofollow,noindex">Combination Operators</a> ---> <a href="/misc/goto?guid=4959717418571543583" rel="nofollow,noindex">zip</a> 以及 <a href="/misc/goto?guid=4959717418656223762" rel="nofollow,noindex">Filtering Operators</a> ---> <a href="/misc/goto?guid=4959717418742821224" rel="nofollow,noindex">skip</a></p>    <pre>  <code class="language-javascript">const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')      .map(e => ({ x: e.offsetX, y: e.offsetY }))    const diff$ = move$      .zip(move$.skip(1), (first, sec) => ([ first, sec ]))</code></pre>    <p>此时的 marble 图:</p>    <pre>  <code class="language-javascript">--x1---x2-x3--x4----x5---|-->  (move$)          skip(1)  -------x2-x3--x4----x5---|-->          --x1---x2-x3--x4----x5---|-->  (move$)  -------x2-x3--x4----x5---|-->            zip    -------x1-x2--x3----x4---|-->  (diff$)         x2 x3  x4    x5</code></pre>    <p>这样一来,diff$ emit 的值就依次为(x1, x2),(x2,x3),(x3,x4) …… 现在,鼠标移动的时候,就可以 <a href="/misc/goto?guid=4959717418408268534" rel="nofollow,noindex">画出美丽的线条</a> 。</p>    <h3><strong>Step 3</strong></h3>    <p>( 我想看 <a href="/misc/goto?guid=4959717418842985259" rel="nofollow,noindex">DEMO</a> )</p>    <p>就在此时我恍然大悟,终于知道前面用 bufferCount 为什么不行了。我们不妨来比较一下:</p>    <pre>  <code class="language-javascript">-------x1-----x3----x5---|---> (bufferCount)         x2     x4    -------x1-x2--x3----x4---|-->  (diff$)         x2 x3  x4    x5</code></pre>    <p>bufferCount emit 的值依次为:(x1,x2),(x3,x4)…… x2和x3之间是有间隔的。这就是为什么线段会不连续的原因。</p>    <p>然后看 <a href="/misc/goto?guid=4959717418238229331" rel="nofollow,noindex">bufferCount</a> 文档的话,你会发现 <strong> 可以使用 bufferCount(2,1) 实现同样的效果</strong>。这样的话,我们就不需要使用zip来组合两个stream。Cool </p>    <pre>  <code class="language-javascript">const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')      .map(e => ({ x: e.offsetX, y: e.offsetX }))    .bufferCount(2, 1)</code></pre>    <p>此时的marble图:</p>    <pre>  <code class="language-javascript">--m1---m2-m3--m4----m5---|-->  (mousemove)          map    --x1---x2-x3--x4----x5---|-->  (点坐标)        bufferCount(2, 1)    -------x1-x2--x3----x4---|---> (两点坐标)         x2 x3  x4    x5</code></pre>    <h2><strong>Step 4</strong></h2>    <p>( 我就要看 <a href="/misc/goto?guid=4959717418934520279" rel="nofollow,noindex">DEMO</a> )</p>    <p>接下来,我们想实现「只有鼠标按下时,才能画画,否则不能」。</p>    <p>首先我们需要 <strong>创建</strong> 两个关于鼠标动作的 stream 。</p>    <pre>  <code class="language-javascript">const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')    const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')</code></pre>    <p>当鼠标按下的时候,我们需要把他 <strong>变换</strong> 成鼠标移动的 stream ,直到鼠标放开。</p>    <p>查看文档 <a href="/misc/goto?guid=4959717418058391866" rel="nofollow,noindex">Transformation Operators</a> ---> <a href="/misc/goto?guid=4959717419026938903" rel="nofollow,noindex">switchMapTo</a> 。</p>    <pre>  <code class="language-javascript">down$.switchMapTo(move$)</code></pre>    <p>此时的 marble 图:</p>    <pre>  <code class="language-javascript">--d---d-d-----d---d--|-->  (mousedown)        switchMapTo    --m---m-m-----m---m--|--></code></pre>    <p>此时,鼠标放开了我们还能 <a href="/misc/goto?guid=4959717418934520279" rel="nofollow,noindex">继续画画</a> ,这显然不是我们想要的。这个时候我们很容易会使用 <a href="/misc/goto?guid=4959717419113940258" rel="nofollow,noindex">takeUntil</a> 这个 operator ,但是这是不对的,因为他会把 <em>stream</em> complete 掉。</p>    <p>还是让我们看看别人是怎么写的吧: <a href="/misc/goto?guid=4959717418329214113" rel="nofollow,noindex">canvas paint</a> 。</p>    <h3><strong>Step 5</strong></h3>    <p>( 我只想看 <a href="/misc/goto?guid=4959717419209575748" rel="nofollow,noindex">DEMO</a> )</p>    <p>思路是这个样子的:</p>    <p>把up$和down$<strong>组合</strong>成一个新的stream,但为了分辨他们,我们需要先把他们<strong>变换</strong>成新的stream 。</p>    <p>查看文档 <a href="/misc/goto?guid=4959717418492069198" rel="nofollow,noindex">Combination Operators</a> ---> <a href="/misc/goto?guid=4959717419301050132" rel="nofollow,noindex">merge</a> 。</p>    <p><a href="/misc/goto?guid=4959717418058391866" rel="nofollow,noindex">Transformation Operators</a> ---> <a href="/misc/goto?guid=4959717418145765344" rel="nofollow,noindex">map</a> 。</p>    <pre>  <code class="language-javascript">const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')      .map(() => 'down')  const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')      .map(() => 'up')    const upAndDown$ = up$.merge(down$)</code></pre>    <p>再来看看他们的 marble 图:</p>    <pre>  <code class="language-javascript">--d--d-d----d--d---|-->  (down$)  ----u---u-u------u-|-->  (up$)        merge    --d-ud-du-u-d--d-u-|-->  (upAndDown$)</code></pre>    <p>此时,我们再 <strong>变换</strong>upAndDown$ 。如果是down的话,则变换成move$,否则变换成一个空的stream 。</p>    <p>查看文档 <a href="/misc/goto?guid=4959717417436577163" rel="nofollow,noindex">Creation Operators</a> ---> <a href="/misc/goto?guid=4959717419410618526" rel="nofollow,noindex">empty</a> 。</p>    <p><a href="/misc/goto?guid=4959717418058391866" rel="nofollow,noindex">Transformation Operators</a> ---> <a href="/misc/goto?guid=4959717419506172863" rel="nofollow,noindex">switchMap</a> 。</p>    <pre>  <code class="language-javascript">upAndDown$      .switchMap(action =>      action === 'down' ? move$ : Rx.Observable.empty()    )</code></pre>    <p>你要的 marble 图:</p>    <pre>  <code class="language-javascript">--d-ud-du-u-d--d-u-|-->  (upAndDown$)      switchMap    --m-em-me-e-m--m-e-|--></code></pre>    <p>其实这个canvas画板不用RxJS实现也不会很难。但是当我们把他扩展成一个「你画我猜」之后,用RxJS处理异步就会变得简单起来。比如,添加新的工具栏(调色板,撤销…… ) ,即时通信(同步画板,聊天) ……</p>    <p>另外,如果你想边学习 RxJS 边实现一些小东西的话:</p>    <ul>     <li><a href="/misc/goto?guid=4959717419592953615" rel="nofollow,noindex">staltz - rxjs training</a></li>     <li><a href="/misc/goto?guid=4959628481675151351" rel="nofollow,noindex">GitHub - Who to Follow</a></li>     <li><a href="/misc/goto?guid=4959717419701785209" rel="nofollow,noindex">RxJS 4.x Example</a></li>     <li><a href="/misc/goto?guid=4959717419784356239" rel="nofollow,noindex">RxJs Playground</a></li>     <li><a href="/misc/goto?guid=4959717419862831414" rel="nofollow,noindex">Yet Another RSS Reader</a></li>     <li><a href="/misc/goto?guid=4959717419944019752" rel="nofollow,noindex">rx-ifying a chat room built with reactjs and socket io</a></li>     <li><a href="/misc/goto?guid=4959717420033416856" rel="nofollow,noindex">angular2-hacknews</a></li>    </ul>    <h2><strong>Production</strong></h2>    <p>怎么把RxJS应用到实际生产的web应用当中呢?</p>    <p>怎么结合到当前流行的框架当中呢?</p>    <h3><strong>Vue</strong></h3>    <p>你可以直接在各种 <a href="/misc/goto?guid=4959717420116678312" rel="nofollow,noindex">Lifecycle Hooks</a> 中使用 RxJS 。</p>    <p>比如created的时候初始化一个Observable,beforeDestroy时就取消订阅Observable。( 查看 <a href="/misc/goto?guid=4959717420194737280" rel="nofollow,noindex">DEMO</a> )</p>    <pre>  <code class="language-javascript">new Vue({      el: '#app',    data: {      time: ''    },      created () {      this.timer$ = Rx.Observable.interval(1000)        .map(() => new Date())        .map(d => moment(d).format('hh:mm:ss'))        .subscribe(t => {          this.time = t        })    },      beforeDestroy () {      this.timer$.unsubscribe()    }  })</code></pre>    <p>其实已经有对应的插件<a href="/misc/goto?guid=4959717420270898551" rel="nofollow,noindex">vue-rx</a>帮我们干了上面的dirty work。他会分别在init和beforeDestroy的时候自动地订阅和取消订阅 Observable :<a href="/misc/goto?guid=4959717420356266355" rel="nofollow,noindex">Vue.js + RxJS binding mixin in 20 lines</a> 。</p>    <p>因此,我们可以直接把一个Observable写到data中:<a href="/misc/goto?guid=4959717420442728758" rel="nofollow,noindex">vue-rx/example.html</a> 。</p>    <h3><strong>React</strong></h3>    <p>类似地,React也可以在他组件的<a href="/misc/goto?guid=4959717420516786189" rel="nofollow,noindex">lifecycle hooks</a>里调用RxJS:<a href="/misc/goto?guid=4959717420613590994" rel="nofollow,noindex">fully-reactive-react</a>。也可以使用<a href="/misc/goto?guid=4959717420693835471" rel="nofollow,noindex">rxjs-react-component</a>把 Observable 绑定到 state 。 如果你结合 Redux 的话,可以使用这个 <a href="/misc/goto?guid=4959717420773255676" rel="nofollow,noindex">redux-oservable</a> 。</p>    <h3><strong>Angular2</strong></h3>    <p>RxJS已经是Angular2的标配,不多说。</p>    <p>更多可查看对应的文档<a href="/misc/goto?guid=4959717420861974413" rel="nofollow,noindex">Angular2 - Server Communication</a> 。</p>    <p>更多关于RxJS的集成:<a href="/misc/goto?guid=4959717420944803210" rel="nofollow,noindex">RxJS community</a> 。</p>    <h2><strong>You Might Not Need RxJS</strong></h2>    <p>根据 <a href="/misc/goto?guid=4959717421017000839" rel="nofollow,noindex">When to Use RxJS</a> ,我们可以知道RxJS的适用场景是:</p>    <ul>     <li> <p>多个复杂的异步或者事件组合在一起</p> </li>     <li> <p>处理多个数据序列(有一定顺序)</p> </li>    </ul>    <p>我觉得,如果你没被异步问题困扰的话,那就不要使用RxJS吧,因为Promise已经能够解决简单的异步问题了。至于Promise和 Observable的区别是什么呢?可以看 <a href="/misc/goto?guid=4959717421107701078" rel="nofollow,noindex">Promise VS Observable</a> 。</p>    <p>讲真,<strong>RxJS 在实际生产中适用的业务场景有哪些</strong>?哪些场景是需要多个异步组合在一起的?游戏吗?即时通信?还有一些特殊的业务。是我的写的业务太少了吗?还是我平时写业务的时候,为写而写,没有把他们抽象起来。</p>    <p>另外,我倒是对 Teambition 关于 RxJS 的思路有点感兴趣: <em> <a href="/misc/goto?guid=4959717421200905352" rel="nofollow,noindex">xufei - 数据的关联计算 -> Brooooooklyn 评论</a> </em> & <a href="/misc/goto?guid=4959717421275092995" rel="nofollow,noindex">xufei - 对当前单页应用的技术栈思考</a> 。</p>    <h2><strong>Summary</strong></h2>    <ul>     <li> <p>RxJS 是用来解决异步和事件组合问题</p> </li>     <li> <p>Observable = <a href="/misc/goto?guid=4959717417011932155" rel="nofollow,noindex">异步数组</a> = 数组 + 时间轴 = stream</p> </li>     <li> <p>Operators = <a href="/misc/goto?guid=4959717417436577163" rel="nofollow,noindex">分类别</a> + <a href="/misc/goto?guid=4959717417097542244" rel="nofollow,noindex">画 marble 图</a> + <a href="/misc/goto?guid=4959717417533214332" rel="nofollow,noindex">看例子</a> + <a href="/misc/goto?guid=4959717417620401627" rel="nofollow,noindex">选</a></p> </li>     <li> <p><strong>更多更详细的更准确的请看 <a href="/misc/goto?guid=4959007487147762984" rel="nofollow,noindex">文档</a> ! </strong></p> </li>    </ul>    <p> </p>    <p>来自:https://fe.ele.me/let-us-learn-rxjs/</p>    <p> </p>