Rxjs科普&在React hooks项目的使用探索

2,185 阅读3分钟

什么是RxJS?

ReactiveX: Reactive Extensions的缩写,一般简写为Rx

Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流

ReactiveX宣言

ReactiveX不仅仅是一个编程接口,它是一种编程思想的突破,它影响了许多其它的程序库和框架以及编程语言。

可见,rxjs几乎精通所有编程语言!当代奇才!

好吧,那什么是rxjs

理解rxjs的逻辑

ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events.

ReactiveX结合了Observer模式和Iterator模式,以及函数式编程和集合,以满足对管理事件序列的理想方式的需求。

每一个Observable都有一条时间线,在这条时间线上可能发出多个事件,下图箭头前面的竖线代表这个Observable在这个时间点complete了,也就代表它的时间线结束了,以后不会再有事件发出。

Observable

SingleMultiple
PullFunctionIterator
PushPromiseObservable

它是可观察对象,上面的表格说明了,可以理解成是一个可以发送多次数据/状态的Promise

值得一提的是虽然名字叫Observable,但是它并不是观察者模式的标准实现,它不会记录谁观察了它(其实是不需要记录,因为每一个观察者得到的都是一个新的流-从头开始播!(其实就是新执行一次Observable的构造器参数中传入的函数),感兴趣可以了解单播多播的区别Discussion: Multicast vs. Unicast · Issue #66 · tc39/proposal-observable

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {

  subscriber.next(1);

  subscriber.next(2);

  subscriber.next(3);

  setTimeout(() => {

    subscriber.next(4);

    subscriber.complete();

  }, 1000);

});

observable.subscribe(observer)

Observer

观察者,没啥好说的,就相当于Promise的then,catch,finally

const observer = {

  next: x => console.log('Observer got a next value: ' + x),

  error: err => console.error('Observer got an error: ' + err),

  complete: () => console.log('Observer got a complete notification'),

};

Operators

定义对流上数据或者行为的操作,就是操作符了。真正理解或者学会rxjs,就看对操作符的掌握了。但是操作符很多还可以自定义,你并不需要学会所有的操作符再开始使用rxjs,就好像你不需要先精通一门编程语言再开始编程一样。其实rxjs很简单!非常简单!

import { of } from 'rxjs';

import { map } from 'rxjs/operators';



of(1, 2, 3)

  .pipe(map((x) => x * x))

  .subscribe((v) => console.log(`value: ${v}`));

// Logs:

// value: 1

// value: 4

// value: 9

操作符广义上分为两种,一种是创建类的操作符,这种操作符不需要在pipe里,就像上面的of一样,他会直接返回一个Observable,第二种是非创建类的操作符,需要在pipe里,就像上面的map一样,他需要一个Observable作为输入,输出一个新的Observable。

Subscription

记录订阅行为的一个对象,通常用来取消订阅(有别的用法不过暂时不需要关注)

import { interval } from 'rxjs';

const observable = interval(1000);

const subscription = observable.subscribe(x => console.log(x));



subscription.unsubscribe();

Subject

它也是一个可观察对象,它就是观察者模式的标准实现了,记录所有观察它的观察者,同时它也是Observer可以观察别的可观察对象,多播。It is hot,Observable is clod(冷热的区别benlesh.medium.com/hot-vs-cold…

import { Subject } from 'rxjs';

 

const subject = new Subject<number>();

 

subject.subscribe({

  next: (v) => console.log(`observerA: ${v}`)

});

subject.subscribe({

  next: (v) => console.log(`observerB: ${v}`)

});

 

subject.next(1);

subject.next(2);

 

// Logs:

// observerA: 1

// observerB: 1

// observerA: 2

// observerB: 2

好了你现在学会了rxjs了!

好吧还需要介绍一些常用/简单的操作符来让大家快速上手使用一下。

创建类操作符
  • fromEvent
  • generate
  • interval
  • of
Transformation | Filtering
  • map
  • filter
  • debounceTime
  • throttleTime
  • switchMap
  • mergeMap
  • exhaustMap

操作符对比

感觉比较容易出现概念不清的就是如下几个了

swtichMap, exhaustMap, mergeMap, concatMap

这三个操作符都是针对higher-order-observable的,会订阅source observable,每当source observable 发送一个事件,就会发送一个新的Observable,然后打平整个流,让高阶流变成正常流。只不过内部的处理逻辑不同。

可以先看一个switchAll的弹珠图帮助理解higher-order-observable

再附上三个对应的弹珠图,来方便解释

swtichMap

简单说就是新的observable来了就不要老得了,喜新厌旧,渣男, 呸!

mergeMap

简单说就是小孩子才做选择,成年人当然是选择全都要,渣男, 呸!

exhaustMap

简单说就是上一个Observable没结束就不接收下一个Observable,这得错过多少好姑娘啊,老实人,活该你接盘,呸!

其实除了Map系列用来处理Higher-order Observables以外还存在All系列,如:swichAll。大家可以自行了解。

concatMap

如果别的女孩给你抛媚眼了先让她等等,等你跟现女友分手之后再无缝衔接

pipe原理

直接上源码,就是一个reduce,没啥黑魔法。函数式编程基本操作,也从侧面说明了Operator应该是一个纯函数。

  pipe(...operations: OperatorFunction<any, any>[]): Observable<any> {

    return pipeFromArray(operations)(this);

  }



/** @internal */

export function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {

  if (fns.length === 0) {

    return identity as UnaryFunction<any, any>;

  }



  if (fns.length === 1) {

    return fns[0];

  }



  return function piped(input: T): R {

    return fns.reduce((prev: any, fn: UnaryFunction<T, R>) => fn(prev), input as any);

  };

}

概念上和MobX的差异

见: 什么是RxJS,我个人的理解就是,RxJS更底层,更灵活,更适合用来 处理-异步-数据-流

MobX 对自己的定位就是专门用来做状态管理的。

比如说。我对MobX的状态管理方案不满意,我可以用RxJS自定义一个,但是做的是不是比MobX更好,就不一定了。我认为,RxJS更通用/底层一些。

适用场景和实践

个人觉得至少在我们业务系统中可以有如下几个用处:

状态管理:

  export function useSubscribe<T>(

    observable: Observable<T>,

    callback: (value: T) => void

  ): void {

    const ref = useRef(callback);

    ref.current = callback;

    useEffect(() => {

      const subscription = observable.subscribe((value) => {

        ref.current(value);

      });

      return () => {

        subscription.unsubscribe();

      };

    }, [observable]);

  }

  

  export function useRxState<T>(subject: BehaviorSubject<T>): T;

  

  export function useRxState<T>(subject: Subject<T>, initState: T): T;

  

  export function useRxState<T>(subject: Subject<T>): T | undefined;

  

  export function useRxState<T>(

    subject: Subject<T>,

    initState?: T

  ): T | undefined {

    const [state, setState]: [T, Dispatch<T>] = useState(

      subject instanceof BehaviorSubject

        ? cloneDeep(subject.getValue())

        : cloneDeep(initState)

    );

    useSubscribe(subject, (value) => {

      setState(cloneDeep(value));

    });

    return state;

  }



  export function selector<T, K>(fn: (value: T) => K) {

    return (observable: Observable<T>): Observable<K> =>

      observable.pipe(

        map((value) => fn(value)),

        distinctUntilChanged((x, y) => isEqual(x, y))

      );

  }

复杂异步交互:网络请求

  export function fetchOperator<T = any>(request: RequestInfo, requestInit: RequestInit) {

      return new Observable<T>((subscriber) => {

          const abortController = new AbortController();

          requestInit.signal = abortController.signal;

          fetch(request, requestInit).then((res) => {

            return res.json();

          }).then((result) => {

            subscriber.next({

                data: result

            });

            subscriber.complete();

          }).catch((err) => {

            subscriber.error(err);

          });

          return () => {

            if (!abortController.signal.aborted) {

              abortController.abort();

            }

          }

      })

  }



  export function fetchStream<R>(request: RequestInfo, requestInit: RequestInit, mapOprator: (project: (value: any, index: number) => any) => OperatorFunction<any, any> = switchMap) {

    return (observable: Observable<any>) => {

      return observable.pipe(mapOprator((value, index) => {

        let requestClone = cloneDeep(request);

        const requestInitClone = cloneDeep(requestInit);

        if (requestInitClone.method === 'GET') {

          if (typeof requestClone === 'string') {

            if (requestClone.indexOf('?') === -1) {

              requestClone = `${requestClone}?${qs.stringify(value)}`

            } else {

              requestClone = `${requestClone}&${qs.stringify(value)}`

            }

          }

        } else {

          requestInitClone.headers = Object.assign({ 

            'Content-Type': 'application/json'

          }, requestInitClone.headers);

          if (!requestInitClone.body) {

            requestInitClone.body = JSON.stringify(value);

          }

        }

        if (!requestInitClone.credentials) {

          requestInitClone.credentials = 'include';

        }

        return fetchOperator<R>(requestClone, requestInitClone)

      }));

    }

  }

抽象复杂业务逻辑(管理副作用?)

其实这三个使用方法都已经有了比较成熟的一些方案,在angular的生态中有ngrs,ngxs。react生态中也有react-rxjs,longyinan大佬写的sigi等。我这里只是一个简单例子

import React, { useState, useEffect, useRef } from 'react';

import logo from './logo.svg';

import './App.css';

import { fetchStream, useBehaviorSubject, useSubject } from './rx/useRx';

import { debounce, debounceTime } from 'rxjs';



interface ResultType {

    data: number;

}



function useBusinessEffect<T extends HTMLElement>(): [React.RefObject<T>, number | undefined] {

    const state$ = useSubject<{

        id: number;

        name: string;

    }>();

    const [data, setData] = useState<number>();

    const [loading, setLoading] = useState<boolean>(false);

    useEffect(() => {

        const subscription = state$.pipe(

            debounceTime(500), 

            fetchStream<ResultType>('https://www.fastmock.site/mock/4779aa4fe803e0771d94a8708f315730/test/api/test01', { method: 'GET' })).subscribe({

            next: (result) => {

                    setData(result);

            },

            complete: (...args) => {

                console.log('complete', args);

            },

            error: (...args) => {

                console.log('error', args);

            }

        })

        return () => {

            subscription.unsubscribe();

        }

    }, []);

    const ref = useRef<T>(null);

    useEffect(() => {

        const cb = () => {

            const num = Math.round(Math.random() * 100)

            state$.next({

                id: num,

                name: `${num}`

            })

        }

        ref.current?.addEventListener('click', cb);

        return () => {

            ref.current?.removeEventListener('click', cb);

        }

    }, []);

    return [ref, data];

}



function App() {

    const [ref, data] = useBusinessEffect<HTMLButtonElement>();

    return (

        <div className="App">

            <header className="App-header">

                <button ref={ref}>点击</button>

                <div>data: {data}</div>

            </header>

        </div>

    );

}



export default App;

写在最后

字节跳动房产垂类招人,前端、node、webgl方向hc都挺多,在线等,挺急的。 如有意向可以扫描下方二维码,或者直接将简历发送到我的邮箱:qinyequan@bytedance.com

image.png