Эта статья была написана членом командынеограниченныйОн был уполномочен для эксклюзивного использования переднего конца Туя, в том числе, но не ограничиваясь редактированием, маркировкой оригинальности и других прав.
В этой статье в основном рассказывается, как использовать 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, нужно решить три проблемы:
- Где определяются данные для рендеринга пользовательского интерфейса?
- Где создается поток Rxjs?
- Как создаются потоки 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);
- Ключ с таким же именем, определяемый как Response data, висит на экземпляре vm, то есть здесь
- Через 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, те же:
- Где определить данные конечного потребления и подготовить котлован;
- Логика потока: построение потока, что такое поток => выполнение потока => подписка на данные, назначение данных;
- Лучшее покрытие сцены: как реализовать управление зависимостями и поведением;
Наконец, надеюсь, что Rxjs сможет творить чудеса в вашей ежедневной разработке вашего фреймворка!