Skip to content

WangYuLue/simple-rxjs

Repository files navigation

简介

200行代码理解 RxJS 的核心概念

RxJS 非常强大,各种操作符连接在一起便能让数据流动到需要用到它的地方,有人甚至觉得 RxJS 是魔法;

然而,太阳底下无新事,本教程将一步步揭开 RxJS 的神秘面纱;

本教程会通过 10 个有趣的小 demo 渐进式的实现 RxJS 的核心功能,其中包括:

  • 类:Observable 的实现
  • 类的方法:subscribe,pipe 的实现
  • 创建类操作符:of,from,fromEvent,interval,timer 的实现
  • 过滤类操作符:filter,take 的实现
  • 工具类操作符:tap 的实现
  • 组合类操作符:merge 的实现

其中还包括 RxJS v5 链式调用 和 RxJS v6 通过 pipe 来调用 的各自实现。

而这一切,只有200行不到的代码,如果感兴趣,开始你的愉快之旅吧!

写在前面

如何使用?

首先安装依赖:

yarn

需要注意的是,本教程的demo是用 typescript 写的;

所以,为了方便调试运行,建议使用 ts-node,可以通过如下方式安装:

yarn global add ts-node

安装好后,你可以用 ts-node 来运行 typescript 文件,例如:

ts-node demo01/index.ts 

这样就可以看到运行结果了;

文件结构

每个 demo 文件夹下会有 4个文件,其中:

  • core.ts 是当前阶段实现的rxjs的核心代码
  • index.ts 是引用官方rxjs写的测试代码
  • index1.ts 是引用前阶段实现的rxjs写的测试代码
  • readme.md 是对于当前阶段代码改动的介绍

可以发现 index.tsindex1.ts 中,除了引用不同,代码没有任何不同。

这样做是为了方便对比官方rxjs和自己实现rxjs的运行效果。

为了保证阅读效果,建议读者边阅读边动手实操,点击这里可以下载源码。

1、实现一个 Observable

RxJS 的一切起源与 Observable,Observable 是 RxJS 世界的基石,没有它,响应式无重谈起。

Observable 表示一个可观察对象,他表示一个可调用的未来值或事件的集合。

比如有以下代码:

import { Observable } from 'rxjs';

const dataStream$ = new Observable(observer => {
  observer.next(1);
  setTimeout(() => {
    observer.next(2);
    observer.complete();
  }, 1000)
  observer.next(3);
});

const observer = {
  next: x => console.log(x),
  error: err => console.error(err),
  complete: () => console.log('done'),
}

dataStream$.subscribe(observer);

这段代码引用的是官方的 Observable, 它在运行后会首先打印一个1,接着打印一个3,隔一秒后会再打印一个2,最后运行结束

仔细观察 Observable 方法,他会接受一个方法传进它的构造函数,这个方法接受一个对象,对象上有 next, error, complete 等属性,但是这个对象是 Observable 实例在调用 subscribe 方法时才传进去的:

有了上面的思路,我们可以大胆的构造出自己的 Observable, 如下:

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }
  subscribe(observer) {
    this._subscribe(observer);
  }
}

把官方的 Observable 替换成自己的 Observable 会发现输出没什么差异。

没错,Observable 的核心就是这么简单。到现在,我们只用了9行代码实现便了Rxjs的核心概念之一 —— Observable

2、实现创建类操作符 of

创建类操作符中,最容易理解的莫过于 of,那么我们就先实现 of 操作符。

比如有如下代码:

import { of } from 'rxjs';

const dataStream$ = of(1, 2, 3)

const observer = {
  next: x => console.log(x),
  error: err => console.error(err),
  complete: () => console.log('done'),
}

dataStream$.subscribe(observer);

它在运行后会首先打印一个1,接着打印一个2,再会打印一个3,最后运行结束。

有了前面自己实现的 Observableof 的实现就会变得非常简单,它实际上只是 Observable 外套了一层包装,本质上还是 Observable,实现如下:

export function of(...args) {
  return new Observable(observer => {
    args.forEach(arg => {
      observer.next(arg);
    })
    observer.complete();
  })
}

把官方的 of 替换成自己的 of ,再配上自己实现的 Observable,我们会发现输出和官方一致。

3、Observable.subscribe 可以传入一个方法作为参数

官方 Observablesubscribe 可以传入一个函数进去,这样的话写起来会清爽很多,如下:

import { of } from 'rxjs';

const dataStream$ = of(1, 2, 3)

dataStream$.subscribe(console.log);

为了我们的 Observable 也能这样的好用,我们可以将 subscribe 适当的改造一下,如下:

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }
  subscribe(observer) {
    const defaultObserver = {
      next: () => { },
      error: () => { },
      complete: () => { }
    }
    if (typeof observer === 'function') {
      return this._subscribe({ ...defaultObserver, next: observer });
    } else {
      return this._subscribe({ ...defaultObserver, ...observer });
    }
  }
}

这样我们的 subscribe 也能只传一个方法进去了,变得相当好用。

4、实现创建类操作符 fromEvent

但是,Rxjs 核心要解决的是数据流传输的问题,很多时候,我们的数据源头来自用户的人机交互,比如说点击按钮,这样的话就不得不用到 fromEvent,比如如下代码:

import { fromEvent } from 'rxjs';
import { JSDOM } from 'jsdom';

const element = new JSDOM(`<div>Hello world</div>`).window.document.querySelector('div');

const source$ = fromEvent(element, 'click');

source$.subscribe(console.log);

setTimeout(() => {
  element.click()
}, 1000)

为了方便对比和测试,我们引用了 jsdom,它的作用是在 node 端可以做一些 dom 的相关操作。

以上代码渲染了一个 Hello world 的元素盒子,并且在一秒钟之后会点击这个盒子。与此同时,我们又使用了 Rxjs 中的 fromEvent 来监听盒子的事件。

为了实现自己的fromEvent,我们来分析一下 fromEvent 所需要的参数,第一个传的是 dom 元素的实例,第二个则是事件的类型,于是可以猜到, fromEvent 内部本质上还是通过原生的 addEventListener 来实现的。而且需要注意到,除非自己手动取消订阅,否则fromEvent创造的对象永远不会结束,根据这个推测,我们能猜到它的内部很有可能只有 next 方法。

有了上述的推断,我们很容易就写出了一个 fromEvent 方法,如下:

export function fromEvent(element, event) {
  return new Observable(observer => {
    const handler = e => observer.next(e);
    element.addEventListener(event, handler);
  });
}

连我都惊讶它的实现居然如此简短,但是运行的效果和官方的效果完全一致。

5、实现创建类操作符 from、interval、timer

有了上面构造 offromEvent 的基础,那么 fromintervaltimer 也将不在话下。

例如,interval 操作符可以这样实现:

export function interval(delay) {
  return new Observable(observer => {
    let index = 0;
    setInterval((() => {
      observer.next(index++)
    }), delay)
  })
}

timer 操作符可以这样实现:

export function timer(delay) {
  return new Observable(observer => {
    setTimeout((() => {
      observer.next(0)
    }), delay)
  })
}

from 操作符的实现稍微比较复杂,因为它可以接受 Array 或者 Promise 类型的参数:

export function from(param) {
  if (Array.isArray(param)) {
    return new Observable(observer => {
      param.forEach(val => observer.next(val));
      observer.complete();
    });
  }
  return new Observable(observer => {
    Promise.resolve(param)
      .then(val => {
        observer.next(val);
        observer.complete();
      })
      .catch(e => {
        observer.error(e);
      });
  })
}

用我们自己实现的 fromintervaltimer 操作符来替换官方的操作符号,会发现执行结果和官方的表现的一致

import { from, timer, interval } from 'rxjs';

const dataStream1$ = from([1, 2, 3]);

const dataPromise = new Promise((res) => {
  setTimeout(() => {
    res('dataPromise');
  }, 1500)
})

const dataStream2$ = from(dataPromise);

const dataStream3$ = timer(1000);

const dataStream4$ = interval(1000);

setTimeout(() => {
  console.log('===== test from =====');
  dataStream1$.subscribe(console.log);
  dataStream2$.subscribe(console.log);
}, 1000)

setTimeout(() => {
  console.log('===== test timer =====');
  dataStream3$.subscribe(console.log);
}, 3000)

setTimeout(() => {
  console.log('===== test interval =====');
  dataStream4$.subscribe(console.log);
}, 5000)

6、为创建类操作符添加取消订阅功能

前面的操作复实现的过于顺利,以至于我隐隐觉得不安,是不是忽略了什么东西。

果然,我马上意识到,事情没有这么简单,上面自己实现的操作符,我无法取消订阅它们,处理不好这会造成严重的内存泄漏。

于是我们着手改造,以 fromEvent 为例,只需要给 Observable 构造函数传入的方法一个返回值,在这个返回值加一个 unsubscribe 属性,然后在这个属性中写入取消订阅的操作。

export function fromEvent(element, event) {
  return new Observable(observer => {
    const handler = e => observer.next(e);
    element.addEventListener(event, handler);
    return {
      unsubscribe: () => element.removeEventListener(event, handler)
    };
  });
}

这样的话,我们的 fromEvent 就能顺利的取消订阅了。

将上面的操作符都整理一下加上返回值,看看我现阶段的成果:

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }
  subscribe(observer) {
    const defaultObserver = {
      next: () => { },
      error: () => { },
      complete: () => { }
    }
    if (typeof observer === 'function') {
      return this._subscribe({ ...defaultObserver, next: observer });
    } else {
      return this._subscribe({ ...defaultObserver, ...observer });
    }
  }
}

export function of(...args) {
  return new Observable(observer => {
    args.forEach(arg => {
      observer.next(arg);
    })
    observer.complete();
    return {
      unsubscribe: () => { }
    }
  })
}

export function fromEvent(element, event) {
  return new Observable(observer => {
    const handler = e => observer.next(e);
    element.addEventListener(event, handler);
    return {
      unsubscribe: () => element.removeEventListener(event, handler)
    };
  });
}

export function from(param) {
  if (Array.isArray(param)) {
    return new Observable(observer => {
      param.forEach(val => observer.next(val));
      observer.complete();
      return {
        unsubscribe: () => { }
      }
    });
  }
  return new Observable(observer => {
    let canceld = false;
    Promise.resolve(param)
      .then(val => {
        if (!canceld) {
          observer.next(val);
          observer.complete();
        }
      })
      .catch(e => {
        observer.error(e);
      });
    return {
      unsubscribe: () => { canceld = true }
    }
  })
}

export function interval(delay) {
  return new Observable(observer => {
    let index = 0;
    const time = setInterval((() => {
      observer.next(index++)
    }), delay)
    return {
      unsubscribe: () => clearInterval(time)
    }
  })
}

export function timer(delay) {
  return new Observable(observer => {
    const time = setTimeout((() => {
      observer.next(0)
    }), delay)
    return {
      unsubscribe: () => clearTimeout(time)
    }
  })
}

目前,我们用不到100行的代码基本实现了 ObservableoffromEventfromintervaltimer,而且实现的效果非常让人惊艳。

但是我们的旅行还没有结束,接下来,笔者将会实现一些好用的转换类操作符,比如 mapfilter。我们将分别用链式调用和pipe调用来分别实现他们,以及对比这两种方式使用的优劣。通过这些对比,我们会明白为什么 rxjs6 为什会使用 pipe 调用 而不延续 rxjs5 的链式调用方法。

7、实现转换类操作符 map 和过滤类操作符 filter(链式调用实现)

对于 mapfilter,同学们一定很熟悉,因为在Array的原型上就有这两个方法。map 表示转换,filter表示过滤,在 rxjs 中也是这样的功能,那我们应该如何实现这两个操作符呢?

同学可能暂时一头雾水,但是只要换个问题问,同学们一定手到擒来,怎么用forEach实现Array中的map和filter?,这还不简单,于是大笔一挥,有了如下的代码:

function map(array,fn) {
  const res = [];
  array.forEach(item=>res.push(fn(item)))
  return res;
}

function filter(array,fn) {
  const res = [];
  array.forEach(item=>fn(item)?res.push(item):'')
  return res;
}

有了上面的参考,那我们实现rxjs中的 mapfilter 就有了很多思路,我们先来考虑链式调用的实现。

有经验的同学肯定知道,上古神器 JQuery 就是典型的链式调用。链式调用有很多优点,特别是写起来非常优雅。

以rxjs5为例,有如下的一串代码:

const dataStream1$ = of(1, 2, 3);

dataStream1$
  .map(data => data * 2)
  .filter(data => data > 3)
  .map(data => data + 1)
  .subscribe(console.log)

仔细观察我们会发现,实现链式调用的关键是 mapfilter 的返回值必须也要是 Observable 实例,而且 mapfilter 需要挂载到实例对象上。

有了上面的思路,并且结合前面 Array 中的 mapfilter 的实现,我们尝试写出了 rxjs5 中 mapfilter的实现,如下:

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }

  map(fn) {
    return new Observable(observer => {
      this.subscribe({
        next: val => observer.next(fn(val)),
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  }

  filter(fn) {
    return new Observable(observer => {
      this.subscribe({
        next: val => fn(val) ? observer.next(val) : () => { },
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  }
}

非常完美,之前写的代码片段可以顺利执行,打印出了 5 和 7。

但是我们也发现的了链式调用的缺陷 —— 方法都在实例上

这也就意味着哪怕仅仅用了一个转换操作符,也将会加载全部操作符。如果实例中的方法比较少,这还能忍受。但是像 rxjs 这样的库,转换类操作符和过滤类操作符加起来有几十种,这样的性能影响是无法忽略的。

而且这样的代码组织方式,打包工具的 tree shaking 将无法起作用,于是有了以下的pipe实现。

8、实现转换类操作符 map 和过滤类操作符 filter(pipe调用实现)

pipe 的思想出现的是间比链式调用还要早,早在 Unix 中就有了 pipe,在Linux 中,我们也能经常看到 | 符号。和前端相关的,有经验的开发同学会知道在 Gulp 也使用了大量的pipe的思想。

以一段 Gulp 的脚本为例:

const { src, dest } = require('gulp');
const babel = require('gulp-babel');

exports.default = function() {
  return src('src/*.js')
    .pipe(babel())
    .pipe(dest('output/'));
}

乍一看和链式调用很像,但是由于 pipe 在中间做了一层隔离,实例对象和具体的转换方法解耦,所以完全没有了上面链式调用出现的问题。

rxjs 中的 pipe 实现更进一步,它能接受多个参数,参考如下写法:

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const dataStream1$ = of(1, 2, 3);

const dataStream2$ = dataStream1$
  .pipe(map(data => data * 2))
  .pipe(filter(data => data > 3))
  .pipe(map(data => data + 1))

const dataStream3$ = dataStream1$.pipe(
  map(data => data * 2),
  filter(data => data > 3),
  map(data => data + 1),
)

上面的代码中 dataStream2$dataStream3$ 实现的效果完全一致,所以想要pipe调用,核心在于如何实现这个加强版 pipe 方法。

一次性实现会有难度,我们可以尝试把这个问题分开,先实现只接受一个参数的 pipe,如下:

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const dataStream1$ = of(1, 2, 3);

const dataStream2$ = dataStream1$
  .pipe(map(data => data * 2))
  .pipe(filter(data => data > 3))
  .pipe(map(data => data + 1))

仔细观察 pipe, 我们会发现它接收一个 RxJS 操作符的运行结果作为参数,并返回一个 Observable 实例。

而且 mapfilter 不像之前链式调用那样挂载在 Observable 实例上,而仅仅是一个纯函数。

而且这个纯函数会在传入 pipe 的时候执行一次,目的是把具体 map 的逻辑传进去。然后在 pipe 方法里再执行一次,目的是吐出一个 Observable 以供后续使用。我们可以猜到 mapfilter 这样的操作符实际上是包了两层 "纸" 的 Observable 实例。

有了上面的思路,我们简单推断便可以写出如下的代码(完整的代码查看demo08):

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }

  // 接受单个参数的 pipe 实现
  pipe(operation) {
    return operation(this);
  }
}

export function filter(fn) {
  return (observable) => (
    new Observable(observer => {
      observable.subscribe({
        next: val => fn(val) ? observer.next(val) : () => { },
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

export function map(fn) {
  return (observable) => (
    new Observable(observer => {
      observable.subscribe({
        next: val => observer.next(fn(val)),
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

我们在 pipe 中将 当前的 Observable 实例传递给操作符以生成一个新的 Observable 实例。经过测试发现,可以完美的运行。就这样,我们实现了可以接收单个参数的 pipe。

接下来我们来 实现可以接受多个参数的 pipe,如下:

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const dataStream1$ = of(1, 2, 3);

const dataStream2$ = dataStream1$.pipe(
  map(data => data * 2),
  filter(data => data > 3),
  map(data => data + 1),
)

尝试将上面的 pipe 方法做一些改造,从

pipe(operation) {
  return operation(this);
}

变成:

pipe(operation1,operation2,operation3) {
  return operation3(operation2(operation1(this)));
}

我们发现可以完美的运行接受三个参数的 pipe, 但是这样实在是太丑陋,而且参数是固定的,于是我们可以再优化一下,如下:

pipe(...operations) {
  return operations.reduce((prev, fn) => fn(prev), this);
}

这样,我们就实现了可以接受任意多参数的 pipe 了,最后写下来才发现,原来一行代码就搞定了。

9、tap 、 take 以及 merge 的实现

最后还有 taptake 以及 merge 的实现,实现的原理和前面提到的操作符大同小异。感兴趣的读者可以继续查阅 demo09demo10,由于篇幅原因,这里就不展开讲了。

10、完整的核心代码

最后,完整的核心代码如下:

export class Observable {
  _subscribe;
  constructor(subscribe) {
    this._subscribe = subscribe;
  }

  pipe(...operations) {
    return operations.reduce((prev, fn) => fn(prev), this);
  }

  subscribe(observer) {
    const defaultObserver = {
      next: () => { },
      error: () => { },
      complete: () => { }
    }
    if (typeof observer === 'function') {
      return this._subscribe({ ...defaultObserver, next: observer });
    } else {
      return this._subscribe({ ...defaultObserver, ...observer });
    }
  }
}

export function of(...args) {
  return new Observable(observer => {
    args.forEach(arg => {
      observer.next(arg);
    })
    observer.complete();
    return {
      unsubscribe: () => { }
    }
  })
}

export function fromEvent(element, event) {
  return new Observable(observer => {
    const handler = e => observer.next(e);
    element.addEventListener(event, handler);
    return {
      unsubscribe: () => element.removeEventListener(event, handler)
    };
  });
}

/**
 * @param param array or promise
 */
export function from(param) {
  if (Array.isArray(param)) {
    return new Observable(observer => {
      param.forEach(val => observer.next(val));
      observer.complete();
      return {
        unsubscribe: () => { }
      }
    });
  }
  return new Observable(observer => {
    let canceld = false;
    Promise.resolve(param)
      .then(val => {
        if (!canceld) {
          observer.next(val);
          observer.complete();
        }
      })
      .catch(e => {
        observer.error(e);
      });
    return {
      unsubscribe: () => { canceld = true }
    }
  })
}

export function interval(delay) {
  return new Observable(observer => {
    let index = 0;
    const time = setInterval((() => {
      observer.next(index++)
    }), delay)
    return {
      unsubscribe: () => clearInterval(time)
    }
  })
}

export function timer(delay) {
  return new Observable(observer => {
    const time = setTimeout((() => {
      observer.next(0)
    }), delay)
    return {
      unsubscribe: () => clearTimeout(time)
    }
  })
}

export function filter(fn) {
  return (observable) => (
    new Observable(observer => {
      observable.subscribe({
        next: val => fn(val) ? observer.next(val) : () => { },
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

export function map(fn) {
  return (observable) => (
    new Observable(observer => {
      observable.subscribe({
        next: val => observer.next(fn(val)),
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

export function take(num) {
  return (observable) => (
    new Observable(observer => {
      let times = 0;
      const subscription = observable.subscribe({
        next: val => {
          times++;
          if (num >= times) {
            observer.next(val)
          } else {
            observer.complete()
            subscription.unsubscribe()
          }
        },
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

export function tap(fn) {
  return (observable) => (
    new Observable(observer => {
      observable.subscribe({
        next: val => {
          fn(val);
          observer.next(val);
        },
        error: err => observer.error(err),
        complete: () => observer.complete(),
      })
    })
  )
}

export function merge(...observables) {
  return (observable) => {
    let completeNum = 0;
    if (observable) {
      observables = [observable, ...observables];
    }
    return new Observable(observer => {
      observables.forEach(observable => {
        observable.subscribe({
          next: val => observer.next(val),
          error: err => {
            observables.forEach(observable.unsubscribe());
            observer.error(err)
          },
          complete: () => {
            completeNum++;
            if (completeNum === observables.length) {
              observer.complete()
            }
          },
        })
      })
    })
  }
}

写在最后

我们通过 10 个有趣的小 demo,不到200行的代码 渐进式的实现 RxJS 的核心功能。

希望读者们阅读完这篇文章后对 RxJS 有更深层次的理解。如果有更优雅的实现方式,欢迎留言区多多交流。

这些 demo 的完整代码可以点这里查看,如果感觉这些 demo 写的不错,可以给笔者一个 star,谢谢大家阅读

About

200行代码理解rxjs的核心概念

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published