【硬核】从0到1实现一个简易版的rxjs

前言

声明一下:从今年开始我的文章会对标至P7左右的技术水平,所以如果你想检验你的能力,欢迎点击关注。

你会掌握rxjs意味着什么

如果你的函数注意单一职责,或者说是注意抽象公共函数,那么加上rxjs,你写的函数基本上可以秒杀绝大部分的前端同学。因为rxjs天然的设计上,就能让你的函数符合

  • s 单一职责原则
  • o 开闭原则
  • 迪米特法则(可以简单理解为发布订阅模式产生的通信解耦)
  • 处理复杂的多个异步任务协调的问题。详细参见我之前的文章(rxjs实用案例)

前置知识:函数组合的思想(和rxjs强相关)

首先要简单谈一下业务中如何写出拓展性强的函数,也就是让函数具有可插拔的插件式效果,目前找到的答案就是函数式编程去解决这个问题(仅仅在编写函数时使用)

为什么函数式变成能让你的函数拓展性非常强呢,请看下图

image.png

数据a,经过f1,f2,f3的处理最终产生结果b,也就是说我们可以编写一个函数

// fn 是由f1\f2\f3组合而来
const fn = compose(f1, f2, f3);

// compose函数有非常多场景,我们假设是最简单同步执行的场景
const compose = (...fns) => x => fns.reduce((y, f) => f(y), x);
复制代码

那么如果产品有变动,我们的compose函数预留了拓展的口子体现在,你可以在f1/f2/f3函数的前后加其他函数,达到增强函数的目的,也可以写一个新的fn,可能只会用到f1和f3函数,重新组合新的功能。

这是一个什么结构呢,类似金字塔,基层函数来组合成上层复杂度更高的函数。

你可以选择用ramdajs这个库,我也一直在用,但是ramdajs在我看来有很多函数完全用不上,其次ramdajs无法解决复杂的异步问题,详细参见我之前的文章(rxjs实用案例)随便拿一条出来,promise都需要写的很复杂,rxjs一行代码可能就解决了。

但是rxjs真的很难理解,因为同一个操作符在同步和异步两个场景得出的结果不一样,就造成初学者记忆十分困难,加上网上学习资料匮乏,学习成本非常高。

阅读后的效果

本篇文章参考了很多网上大神的文章总结而来,但也有很多自己原创的部分,虽然是很基本的rxjs实现,但是框架上的结构和6版本的rxjs基本保持一致,基本上保证看懂这个代码,你要理解它异步的流和同步流的执行机制,然后直接上手rxjs,是没有啥问题的,用两周常用的操作符,基本上就是个API熟手了。

开干!rxjs基本结构

我们先实现一个很简单的效果,如下:

const observable = new Observable(function publish(observer) {
    const clearId = setInterval(() => {
        observer.next('hello world');
    }, 1000);
    setTimeout(() => {
        observer.complete();
    }, 1500);
    return () => clearTimeout(clearId);
})

observable.subscrible({
    next: (value) => console.log(value),
    error: (err) => console.log(err),
    complete: () => console.log('done')
})
复制代码

也就是1秒后打印'hello world',并且1.5秒后取消之前的setInterval,并打印done。

首先我们需要实现最简单的Observable类,它的作用就是注事件,类比发布订阅模式,就是注册一个函数,参数是observer,也就是类 - Observer的实例,后面我们会实现,先忽略。

subscrible就是发布事件,真正调用之前注册的函数,这里就是new Observable里注册的函数。

class Observable {
    // 注册监听函数publish
    constructor(publish) {
        this.publish = publish
    }
    // 发布函数,将之前注册的publish函数调用
    subscrible(next, error, complete) {
        let observer;
        // 将subscrible参数转化为Observer的实例
        observer = new Observer(next.next, next.error, next.complete);
        // 将注册的函数调用
        const unsubscribeCb = this.publish(observer);
        // 将取消注册的函数添加到observer实例上
        observer.onUnsubscribe(unsubscribeCb)
        return observer;
    }
}
复制代码

我们接着实现Observer类,用来产生实例给之前注册的函数当做参数调用。

const noop = () => { };
class Observer {
    // 当你调用Observer的unsubscribe方法时,触发调用unsubscribeCb
    unsubscribeCb;
    // next表示要去消费数据的函数
    constructor(next = noop, error = noop, complete = noop) {
        this._next = next;
        this._error = error;
        this._complete = complete;
        // 表示是否已经调用过error或者complete方法了,调用过就不能再次next了
        this.isStopped = false;
    }
    next(value) {
        if (!this.isStopped) {
            this._next(value);
        }
    }
    error(err) {
        if (!this.isStopped) {
            this._error(err);
            this.unsubscribe();
        }
    }
    complete() {
        if (!this.isStopped) {
            this._complete();
            this.unsubscribe();
        }
    }
    unsubscribe() {
        this.isStopped = true;
        this.unsubscribeCb && this.unsubscribeCb();
    }
    onUnsubscribe(unsubscribe) {
        this.unsubscribeCb = unsubscribe;
    }
}
复制代码

好了,兄弟姐们和铁窗(监狱)后面的哥们儿,消化理解一下再继续吧,这是最最最基础的案例了哦!

image.png

我们趁热打铁,接着实现两个Observable的静态方法

  • formEvent
  • interval

从formEevent开始,我们的demo如下,点击body,会打印223:

Observable.fromEvent(document.body, 'click').subscrible({
    next: () => {
        console.log('223')
    }
})
复制代码

实现如下:

Observable.fromEvent = function (target, eventName) {
    // 返回Observable类的实例,因为它必须满足我们发布订阅的框架
    // 框架就是Observable类的实例是注册事件者,调用事件者
    // Observer实例是调用者的参数
    return new Observable((observer) => {
        const handleFn = (e) => {
            observer.next(e);
        }
        target.addEventListener(eventName, handleFn);
        return () => {
            target.removeEventListener(eventName, handleFn)
        }
    })
}
复制代码

接着实现interval,我们的demo如下,会打印0,1,2,然后结束。

const obs = Observable.interval(1000).subscrible({
    next:(index)=>console.log(index)
})
setTimeout(()=>{
    obs.unsubscribe();
}, 3000)
复制代码

我们来实现interval

Observable.interval = function (delay) {
    return new Observable(function (observer) {
        let index = 0;
        const clearId = window.setInterval(() => {
            observer.next(index++)
        }, delay);
        return () => {
            window.clearInterval(clearId)
        }
    })
}
复制代码

好了,这一阶段结束了,我们接下来会完善一下Observabel代码。休息之余,让我给大家弹一首歌曲吧。

我们接下来丰富一下subscrible这个方法吧。我原来是这样的:

class Observable {
    constructor(publish) {
        this.publish = publish
    }
    subscrible(next, error, complete) {
        let observer;
        observer = new Observer(next.next, next.error, next.complete);
        const unsubscribeCb = this.publish(observer);
        observer.onUnsubscribe(unsubscribeCb)
        return observer;
    }
}
复制代码

我们想在调用subscrible的时候,还可以传入Observer的实例,或者函数,大家看下下面代码就马上明白意思喽。

class Observable {
    constructor(publish) {
        this.publish = publish
    }
    // 消费消费者的函数
    subscribe(next, error, complete) {
        let observer;
        // 判断next是什么类型,最终都要转化为Observer的实例
        if (next instanceof Observer) {
            observer = next;
        } else if (typeof next === 'function') {
            observer = new Observer(next, error, complete);
        } else {
            observer = new Observer(next.next, next.error, next.complete);
        }
        const unsubscribeCb = this.publish(observer);
        observer.onUnsubscribe(unsubscribeCb);
        return observer;
    }
}
复制代码

同步函数的rxjs运行流程

这里开始难度上了一个台阶,没理解没关系,多看几遍。

我们最开始的目的实现这样的一个效果,以下代码输出3,5,7

Observable.fromArray([1,2,3,4,5,6])
   .pipe(
     map((item) => item + 1),
     filter((item) => !!(item % 2))
   )
   .subscribe((item) => {
     console.log(item);
   });
复制代码

从上面看,我们多了pipe方法,fromArray方法,还有map和filter操作符,我们先从fromArray方法讲起

fromArray

observer在rxjs里面是消费者的角色,比如说Observable.fromArray([1,2,3,4,5,6]),这里发布了一个数组,我们要消费它,就需要调用subscribe方法去消费这些值,比如说

// 会打印1,2,3,4,5,6
Observable.fromArray([1,2,3,4,5,6]).subscribe((item) => {
     console.log(item);
   });
复制代码

Observable在rxjs里面是生产者的角色,负责生产数据,上面的例子也可以看到fromArray就是一个生产数组数据的函数,所以我们要理解这两个函数的实现。

// 建立静态方法 
Observable.fromArray = function(array) {
    if(!Array.isArray(array)) {
        // 如果传入的参数不是阵列,则抛出例外
        throw new Error('params need to be an array');
    }
    return new Observable(function(observer) {
        try{
            // 遍历每个元素并送出
            array.forEach(value => observer.next(value))
            observer.complete()
        } catch(err) {
            observer.error(err)
        }
    });
}
复制代码

fromArray这个操作符足够简单了吧,接下来我们看看pipe方法, 对于不熟悉函数组合的同学,有点难度哦。

class Observable {
    source; // 新增代码
    operator; // 新增代码
    constructor(publish) {
        this.publish = publish
    }
    // 消费消费者的函数
    subscribe(next, error, complete) {
        let observer;
        // 判断next是什么类型,最终都要转化为Observer的实例
        if (next instanceof Observer) {
            observer = next;
        } else if (typeof next === 'function') {
            observer = new Observer(next, error, complete);
        } else {
            observer = new Observer(next.next, next.error, next.complete);
        }
        // 新增代码
        if(this.operator){
            return this.operator.call(observer, this.source);
        }
        const unsubscribeCb = this.publish(observer);
        observer.onUnsubscribe(unsubscribeCb);
        return observer;
    }
    // 新增代码
    lift(operator){
        const observable = new Observable();
        observable.source = this;
        observable.operator = operator;
        return observable;
    }
    // 新增代码
    pipe(...operations){
        if(operations.length === 0) {
            return this;
        } else if (operations.length === 1) {
            return operations[0](this);
        } else {
            return operations.reduce((source, func) => func(source), this)
        }
    }
}
复制代码

pipe,这个函数组合,实现如下的功能,比如pipe(map. filter),我们简单翻译一下这句话啥意思:

operations.reduce((source, func) => func(source), this)
复制代码

结合pipe(map. filter)(参数),就是

filter(map(参数))
复制代码

看到没,很简单,就是map处理完数据后给filter去处理而已

好了,我们接着看lift函数是干嘛的呢?这个函数要具体结合map操作符的源码一起讲才行,单独拎出来的话,你就记住,这个一个链表结构,lift把函数间的处理变为了链表,上面我们说

filter(map(参数))
复制代码

这种如果改成链表就是

链表元素1包含属性fn:map ---> 链表元素2包含属性fn:filter
复制代码

也就是链表元素1处理完数据丢给链表元素2处理

map操作符实现

// 这个函数的目的是返回一个observable
function map(mapFn) {
    // 这里是在pipe方法的时候调用的
    // 最终返回一个observable, operator属性是new mapOperator(mapFn)
    // source 属性保存上一个 Observable实例,也就是下面source参数
    return function mapOperation(source) {
        return source.lift(new mapOperator(mapFn))
    }
}

class mapOperator {
    constructor(mapFn) {
        this.mapFn = mapFn;
    }
    // call方法最终调用的是source observable的subscribe方法
    // 对传入的observer进行一层封装
    call(observer, source) {
        return source.subscribe(
            {
                next: (value) {
                    observer.next(mapFn(value))
                },
                complete: () {
                    this.observer.complete();
                }
            }
        )
    }
}
复制代码

不理解没关系,我们把filter的实现写完,就梳理一下函数的执行流程,

// filter的实现
function filter(filterFn) {
  // 这里是在pipe方法的时候调用的
  // 最终返回一个observable, operator属性是new mapOperator(mapFn)
  // source 属性保存上一个 Observable实例,也就是下面source参数
  return function mapOperation(source) {
    return source.lift(new filterOperator(filterFn));
  };
}

class filterOperator {
  constructor(filterFn) {
    this.filterFn = filterFn;
  }
  // call方法最终调用的是source observable的subscribe方法
  // 对传入的observer进行一层封装
  call(observer, source) {
    return source.subscribe(
      {
                next: (value) {
                     if(this.filterFn(value)){
                          this.observer.next(this.filterFn(value));
                     }
                },
                complete: () {
                    this.observer.complete();
                }
      }
    );
  }
}

复制代码

以上不理解没关系,我们梳理一下代码,首先我们调用Observable.fromArray([1,2,3]),会返回这样一个实例

{
  operatorundefined
  publish: ƒ (observer) 
   // 这里的publish就是如下函数
   // function (observer) {
   // try {
   //   // 遍历每个元素并送出
   //   array.forEach((value) => observer.next(value));
   //   observer.complete();
   // } catch (err) {
   //   observer.error(err);
   // }
   //}
  sourceundefined
  __proto__:
    constructorclass Observable
    lift: ƒ lift(operator)
    pipepipe(...operations) { if (operations.length === 0) { return this; } else if (operations.length === 1) { return operations[0](this); } else { return operations.reduce((source, func) => {…}
    subscribe: ƒ subscribe(next, error, complete)
    __proto__Object
}
复制代码

我们接着看,下面这段代码执行的流程,我画了一张图

Observable.fromArray([1,2,3,4,5,6])
   .pipe(
     map((item) => item + 1),
     filter((item) => !!(item % 2))
   )
   .subscribe((item) => {
     console.log(item);
   });
复制代码

未命名文件 (13).jpg

所以,我们的next函数调用是这样的,filter的Observer包含了原始的observer,map的observer包含了filter的observer,formArray包含了map的observer,

所以我们调用formArray发布数publish函数时,参数就是map的observer,如下:

next(value) {
    if(filterFn(mapFn(value))){
      原始observer.next(filterFn(mapFn(value)));
    }
}
复制代码

这个真的有点绕,如果真的不理解也没啥,就是知道大概是这么一个流程也行,平时写代码还真借鉴不上。

异步函数处理

上面的操作符都是同步的,我们平时业务遇到的更多是异步的复杂度,异步在rxjs主要处理的思路如下:

  • 会有一个叫做NotifierObserver的类,这个东西是干啥的呢
  • 它叫做通知类,啥意思呢,就是异步完成了,我就通知一下,完成,接着走后面的任务,或者说对之前的任务有一些影响

我们举一个例子,稍微理解一下NotifierObserver类,知道异步这么处理就行,这样你在看到一些比如switchMap这样稍微复杂的操作符,也能联想一下,如果用这个类大概实现的思路。

takeUntil

操作符用法如下,一开始控制台会每隔1秒打印一次数字,但你点击页面后,就停止打印数字:

const notifier = Observable.fromEvent(document, 'click');
const observable = Observable.interval(1000).pipe(takeUntil(notifier))
复制代码

实现takeUntil

function takeUntil(notifier) {

  return function takeUntilOperation(source) {
    
    return source.lift(new takeUntilOperator(notifier));

  };

}



class takeUntilOperator {

  constructor(notifier) {

    this.notifier = notifier;

  }


  call(observer, source) {
     // 这里outerObserver是takeUntilObserver的实例
     // NotifierObserver就是如果满足条件就会通知outerObserver
    const outerObserver = new takeUntilObserver(observer, this.notifier);

    const notifierObserver = new NotifierObserver(outerObserver);
    // 这里指如果this.notifier触发,则会改变outerObserver的状态
    this.notifier.subscribe(notifierObserver);
    // 当outerObserver状态seenValue为false才会执行下面的,否则不执行
    if (!outerObserver.seenValue) {
      return source.subscribe(outerObserver);
    }
  }

}

class NotifierObserver extends Observer {

  constructor(outerObserver) {

    super();

    this.outerObserver = outerObserver;

  }

  // 接受到值就通知outerObserver

  next(value) {

    this.outerObserver.notifyNext(value);

  }

  error(err) {

    this.outerObserver.notifyError(err);

    this.unsubscribe();

  }

  complete() {

    this.outerObserver.notifyComplete();

    this.unsubscribe();

  }

}

class takeUntilObserver extends Observer {

  constructor(destination) {

    super();

    this.destination = destination;

    this.seenValue = false;

  }

  // 接收到notifyNext的值或notifyComplete时就完成订阅

  notifyNext(value) {

    this.seenValue = true;

    this.destination.complete();

  }

  notifyComplete() {

    this.seenValue = true;

    this.destination.complete();

  }

  next(value) {

    if (!this.seenValue) {

      this.destination.next(value);

    }

  }

}
复制代码

上面我们总结一下异步场景,就是有一个通知类,在某种条件下触发,改变了observable的状态,然后这个状态直接影响observer的执行。

我们就可以大胆的假设各种异步操作符,如何在通知类的基础上去工作,比如race(我没看过源码),race的参数是一个observable的数组,哪个observable先触发,则会以这个observable去执行下游的其它操作,其它的observable就丢弃

我们可以猜想就是谁第一个observable触发了,那么通知类就会改变race操作符的状态,让后面的observable无效

好了,文章到此为止。

打个小广告,今年我会有一个大题材,就是手把手教你实现一个可以开源级别的react pc和移动端组件库,包含:

  • 组件库打包,开发,测试,自动化push仓库(包括修改changeLog文件和打tag)的cli工具(已完成100%)
  • 组件库的icon包,也就是所有icon集合在一个服务的npm包里,专属于你们的项目。
  • 组件库网站搭建(自己写,不是用storybook、dumi或者docz)
  • pc端组件库(包括ant所有组件和功能,主要是借鉴其源码,也可以说是源码分析)
  • 移动端组件库(主要借鉴的是zarm,众安的一个react组件库)

参考文章:

猜你喜欢

转载自juejin.im/post/7069686756303437837