Практическое руководство по интерфейсу Rxjs

внешний интерфейс RxJS
Практическое руководство по интерфейсу Rxjs

Эта статья была написана членом командынеограниченныйОн был уполномочен для эксклюзивного использования переднего конца Туя, в том числе, но не ограничиваясь редактированием, маркировкой оригинальности и других прав.

В этой статье в основном рассказывается, как использовать Rxjs во внешних фреймворках React и Vue, и что делается за rxjs-hooks и vue-rx с открытым исходным кодом. Прежде чем начать, я надеюсь, что у вас есть общее представление о реактивном программировании и Rxjs. Давайте начнем!

идеальное партнерство

Обязанности интерфейсных фреймворков (таких как React, Vue): синхронизация данных и UI, при изменении данных UI автоматически обновляется;

UI = f(data)

Что делает реактивное программирование (например, Rxjs): основное внимание уделяется данным, от источника потока данных до обработки данных и подписки на данные (потребление данных);

data = g(source)

Отношения между ними не конфликтуют, и даже в некоторых сценариях это идеальное партнерство.Фронтенд-фреймворк можно использовать в качестве потребителя данных реактивного программирования:

UI = f(g(source))

Это похоже на определение MV:

MVVM happens to be a good fit for Rx*. Quoting Wikipedia:

Модель представления MVVM представляет собой преобразователь значений, что означает, что модель представления отвечает за предоставление объектов данных из модели таким образом, чтобы этими объектами было легко управлять и потреблять.В этом отношении модель представления является скорее моделью, чем представлением. и обрабатывает большую часть, если не всю логику отображения представления.

Начнем с React: rxjs-хуки

В React (рассматриваются только функциональные компоненты) есть две формы прямого выражения «неоднократного назначения»:

  • useMemo
const greeting = React.useMemo(() => `${greet}, ${name}!`, [greet, name]);
  • useState+useEffect
const [greeting, setGreeting] = useState(() => `${greet}, ${name}!`);

useEffect(() => {
  setGreeting(() => `${greet}, ${name}!`);
}, [greet, name]);

Уведомление:useMemoРасчетные данные перед рендерингом, аuseState+useEffectЛогика расчета данныхuseEffect, после рендера.

Если вы хотите получить доступ к Rxjs, вам нужно построить весь «конвейер», включаяObservableподготовка, обработка данных, подписка на данные и даже некоторые побочные эффекты (тап), которые выходят за рамкиuseMemoгрузоподъемность. НапротивuseEffectно это очень подходит (родился для побочных эффектов), попробуйтеuseState+useEffectрасширять.

Сначала идет базовая версия:

import * as React from 'react';
import { combineLatest, from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';

const GreetSomeone = ({ greet = 'Hello' }) => {
    const [greeting, setGreeting] = React.useState('');

    React.useEffect(() => {
        const greet$ = of(greet);
      	// fetchSomeName: 远程搜索数据
        const name$ = from(fetchSomeName()).pipe(
            startWith('World'),
            catchError(() => of('Mololongo')),
        );

        const greeting$ = combineLatest(greet$, name$).pipe(
            map(([greet, name]) => `${greet}, ${name}!`)
        );

        const subscription = greeting$.subscribe(value => {
            setGreeting(value);
        });

        return () => {
            subscription.unsubscribe();
        }
    }, []);

    return <p>{greeting}</p>;
};

Немного похоже, вuseEffectПоток Rxjs встроен, после подписки данные записываются в компонент для рендеринга данных, а при уничтожении компонента подписка отменяется.

Но тут есть проблема, компонент принимаетprop greetизменится, иgreet$Данные не будут обновляться. Как это решить? Если обращаться так:

React.useEffect(() => {
    const greet$ = of(greet);				
    /**
     * 同上,流构建逻辑
    **/
}, [greet]);

Проблема в том, что каждый раз, когда поток Rxjs будетgreetОбновите и перегенерируйте, а затем вызовите интерфейсfetchSomeNameбудет вызван снова. Стоимость немного велика.

Как это решить?

представить еще одинuseEffect, с RxjsSubject.nextАктивно отправляйте данные и убедитесь, что построение потока Rxjs выполняется только один раз, вставьте полный код:

import * as React from 'react';
import { BehaviorSubject, combineLatest, from, of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';

const GreetSomeone = ({ greet = 'Hello' }) => {
    // 使用React.useRef在组件生命周期保持不变
    const greet$ = React.useRef(new BehaviorSubject(greet));

    // Subject.next 推数据,使得Rxjs数据更新
    React.useEffect(() => {
        greet$.current.next(greet);
    }, [greet]);

    const [greeting, setGreeting] = React.useState('');
		
    // 逻辑不变,仅执行一次
    React.useEffect(() => {
        const name$ = from(fetchSomeName()).pipe(
            startWith('World'),
            catchError(() => of('Mololongo')),
        );

        const greeting$ = combineLatest(greet$.current, name$).pipe(
            map(([greet, name]) => `${greet}, ${name}!`)
        );

        const subscription = greeting$.subscribe(value => {
            setGreeting(value);
        });

        return () => {
            subscription.unsubscribe();
        }
    }, [greet$]);

    return <p>{greeting}</p>;
};

Имея общее представление о реализации, основанной на хуках, давайте взглянем на реализацию с открытым исходным кодом.Rxjs-hooks, его введение очень просто:

React hooks for RxJS.

Rxjs-hooks разработал два хука, одинuseObservable,одинuseEventCallback.

посмотриuseObservable: После удаления типа TS, соответствует ли он упомянутой выше структуре?

export function useObservable(
  inputFactory,
  initialState,
  inputs,
){
  const [state, setState] = useState(typeof initialState !== 'undefined' ? initialState : null)
	
  const state$ = useConstant(() => new BehaviorSubject(initialState))
  const inputs$ = useConstant(() => new BehaviorSubject(inputs))

  useEffect(() => {
    inputs$.next(inputs)
  }, inputs || [])

  useEffect(() => {
    let output$
    if (inputs) {
      output$ = inputFactory(state$, inputs$)
    } else {
      output$ = inputFactory(state$) 
    }
    const subscription = output$.subscribe((value) => {
      state$.next(value)
      setState(value)
    })
    return () => {
      subscription.unsubscribe()
      inputs$.complete()
      state$.complete()
    }
  }, []) // immutable forever

  return state
}

Пример использования:

import React from 'react'
import ReactDOM from 'react-dom'
import { useObservable } from 'rxjs-hooks'
import { of } from 'rxjs'
import { map } from 'rxjs/operators'

function App(props: { foo: number }) {
  const value = useObservable((_, inputs$) => inputs$.pipe(
    map(([val]) => val + 1),
  ), 200, [props.foo])
  return (
    // render three times
    // 200 and 1001 and 2001
    <h1>{value}</h1>
  )
}

видимыйuseObservableпротивprops,stateЧтобы построить Observable и, наконец, вернуть подписанные данные. Итак, ввод:inputFactory(то есть логика построения потока Rxjs),initialState,inputs.

useEventCallbackАналогично, за исключением того, что хук возвращает подписанные данные, он также возвращаетcallback, который обрабатывает случай ответа на событие:

const event$ = useConstant(() => new Subject<EventValue>())

function eventCallback(e: EventValue) {
  return event$.next(e)
}

return [returnedCallback as VoidableEventCallback<EventValue>, state]

Мышление: условия, необходимые для посадочной среды rxjs

Оглядываясь назад на реализацию Rxjs в React, нужно решить три проблемы:

  1. Где определяются данные для рендеринга пользовательского интерфейса?
  2. Где создается поток Rxjs?
  3. Как создаются потоки RxjsObservableПродолжать излучать (излучать) из значения и течь?

Практика: Vue + Rxjs

Основываясь на той же идее, попробуйте реализовать использование Rxjs во Vue:

<template>
  <div>{{ greeting }}</div>
</template>

<script>
import { from, combineLatest, BehaviorSubject } from "rxjs";
import { map } from "rxjs/operators";

let subscription = null,
  greet$ = null;

export default {
  name: "TryRxInVue",
  props: {
    greet: {
      type: String,
      default: "hello",
    },
  },
  data() {
    return {
      greeting: "",
    };
  },
  // 监听依赖,使得流动
  watch: {
    greet(value) {
      this.greet$.next(value);
    },
  },
  // 不同生命周期钩子
  mounted() {
    this.initStream();
  },
  beforeDestroy() {
    subscription = null;
    greet$ = null;
  },
  methods: {
    // 初始化流,在组件mounted时调用
    initStream() {
      greet$ = new BehaviorSubject(this.greet);
      const name$ = from(Promise.resolve("world"));

      const greeting$ = combineLatest(greet$, name$).pipe(
        map(([greet, name]) => `${greet},${name}!`)
      );

      subscription = greeting$.subscribe((value) => {
        this.greeting = value;
      });
    },
  },
};
</script>

Обнаружите, что недостаток в том, что логика очень фрагментирована, поэтому нет хорошего пакета?

Механизм плагинов, предоставляемый Vue!

В двух словах: написать конструкцию потока в согласованной позиции конфигурации, транслировать конфигурацию через плагин, и вставить в соответствующий жизненный цикл, мониторинг и т.д. для исполнения.

Сравнение реализации библиотек с открытым исходным кодом

Найдена интеграция Vue.js на основе Rxjs V6, официально реализованная Vue:vue-rx. Как и vue-router, vuex и т. д., это также плагин Vue.

После прочтения исходного кода идея в основном такая же, как и я. Есть следующие важные моменты для записи.

Большая часть ядраsubscriptionsКонфигурация, она используется так:

<template>
  <div>
    <p>{{ num }}</p>
  </div>
</template>

<script>
import { interval } from "rxjs";

export default {
  name: "Demo",
  subscriptions() {
    return {
      num: interval(1000).pipe(take(10))
    };
  },
};
</script>

Что за этим стоит? Как это переводится?

  • Через Mixin, в жизненном циклеcreatedкогда:
    • Ключ с таким же именем, определяемый как Response data, висит на экземпляре vm, то есть здесьnumбудет висетьvm.num;
    • Для каждого об, держисьvm.$observablesвверх, то естьvm.$observables.numЭтот объект можно получить, но он кажется бесполезным...;
    • Выполнить ob, подписку на данные, присвоить такое же имяvm[key],Прямо сейчасvm.numОн привязан к этому ob (Примечание: здесь для виртуальной машины используется объект Subscription, цель которого состоит в том, чтобы подписаться и отписаться от ob);
  • Через Mixin, в жизненном циклеbeforeDestroyкогда: отписаться;

Простой взгляд на нижний исходный код:

import { defineReactive } from './util'
import { Subject, Subscription } from 'rxjs'

export default {
  created () {
    const vm = this

    // subscriptions来来
    let obs = vm.$options.subscriptions
    
    if (obs) {
      vm.$observables = {}
      vm._subscription = new Subscription()
      Object.keys(obs).forEach(key => {

        // 定义了响应式数据,key挂在vm实例上
        defineReactive(vm, key, undefined)
        // obs也挂在了vm.$observables上
        const ob = vm.$observables[key] = obs[key]

        // 执行ob,数据订阅,最后赋值给准备好的obs[key]坑位
        vm._subscription.add(obs[key].subscribe(value => {
          vm[key] = value
        }, (error) => { throw error }))
      })
    }
  },

  beforeDestroy () {
    // 取消订阅
    if (this._subscription) {
      this._subscription.unsubscribe()
    }
  }
}

subscriptionsПосле настройки основная проблема решена, а остальное — как реализовать управление зависимостями и поведением;

Как реализовать управление зависимостями?

vue-rx предоставляет$watchAsObservableметод, его можно использовать следующим образом:

import { pluck, map } from 'rxjs/operators'

const vm = new Vue({
  data: {
    a: 1
  },
  subscriptions () {
    // declaratively map to another property with Rx operators
    return {
      aPlusOne: this.$watchAsObservable('a').pipe(
        pluck('newValue'),
        map(a => a + 1)
      )
    }
  }
})

$watchAsObservableПараметр является выражением и возвращает значение ob. Когда значение выражения изменяется, значение ob всплывает. Его реализация исходного кода вторгаетсяNew Observable({...}):

import { Observable, Subscription } from 'rxjs'

export default function watchAsObservable (expOrFn, options) {
  const vm = this
  const obs$ = new Observable(observer => {
    let _unwatch
    const watch = () => {
      _unwatch = vm.$watch(expOrFn, (newValue, oldValue) => {
        observer.next({ oldValue: oldValue, newValue: newValue })
      }, options)
    }
    
		// 这里简单了一下
    watch()

    // 返回取消订阅
    return new Subscription(() => {
      _unwatch && _unwatch()
    })
  })

  return obs$
}

Такой подход распространен в Vue-RX. Вы обнаружите, что логика такая же, как простая демонстрация, написанная самостоятельно, но логика декларации OB и изменение наблюдаемого значения инкапсулируется в плагин.

Как добиться управляемого поведения?

Простое демо, написанное мной, не включает его, но это не что иное, как определение субъекта, который участвует в построении потока, и когда событие отвечает, он генерирует значения для продвижения изменений данных потока. .

Привет, не говорите, это действительно то, что vue-rx делает за одним из подходов, основанных на поведении, с помощью пользовательских директив.v-stream+настроитьdomStreams, здесь не раскрыто.

Другой способ — экземпляр, выставленный vue-rx.observableMethodsЕго реализация довольно тонкая, просто поговорим об этом. Например, используйте следующее:

new Vue({
  observableMethods: {
    submitHandler: 'submitHandler$'
    // or with Array shothand: ['submitHandler']
  }
})

это будет в МиксинеcreatedВ течение жизненного цикла монтируются два свойства,vm.submitHandler$является об, участвующим в построении потока,vm.submitHandlerЭто производитель данных этого объекта ob, открытый интерфейс, а параметр — значение объекта ob. Такой механизм включает в себя как объявление ob, так и раскрытие метода push ob.next. Недостатком является то, что метод вождения и метод наблюдения недостаточно интуитивны, зависят от условности и познания и недостаточно ясны.

Vue Composition API

Новый Composition API Vue, вдохновленный React Hooks.

Как и хуки React, Vue Composition API также направлен на решение проблемы фрагментации логики.

На основе Vue Composition API есть новое обсуждение того, как интегрировать Rxjs, Преимущество в том, что логика более агрегирована для пользователя.

Подробное обсуждение смотрите здесь:Vue Composition API and vue-rx.

Суммировать

Прежде всего, проясняется взаимосвязь между Rxjs и интерфейсными фреймворками, такими как React/Vue, и они могут быть взаимодействующими в приложении.

Во-вторых, узнайте, как интегрировать Rxjs во внешние фреймворки с помощью rxjs-hooks и vue-rx. Это вопрос поиска наиболее подходящего механизма в рамках заданного фреймворка с хуками, которые React выполняет свою часть, и относительно громоздкими плагинами Vue. Но по сути проблемы, которые нужно решить при интеграции Rxjs, те же:

  1. Где определить данные конечного потребления и подготовить котлован;
  2. Логика потока: построение потока, что такое поток => выполнение потока => подписка на данные, назначение данных;
  3. Лучшее покрытие сцены: как реализовать управление зависимостями и поведением;

Наконец, надеюсь, что Rxjs сможет творить чудеса в вашей ежедневной разработке вашего фреймворка!