[FE] Hello "Observable Hooks"

最近项目中用到了 react + rxjs + observable-hooks,下文总结一下 observable-hooks 的学习心得。

1. 搭建 React 环境

使用 create-react-app 创建 react 项目 test-hooks

$ npm i -g create-react-app
$ create-react-app test-hooks
$ cd test-hooks
$ npm i
$ npm start

我们可以在 src/App.js 中写一些实验代码,例如,

function App() {
  return (
    <>Hello World</>
  );
}
export default App;

2. Observable Hooks

Observable Hooks 是 React hooks for RxJS Observables
官方文档 Core Concepts 中介绍了 Two Worlds 概念模型,非常值得一读。

为了使用 Observable Hooks,需要在上文 test-hooks 项目中,
安装 rxjsobservable-hooks 依赖。

$ npm i -S rxjs observable-hooks

(1)异步取值

功能:页面初始化展示 0,500ms 后展示 1

import { from } from 'rxjs';
import { useObservableState } from 'observable-hooks';

function App() {
  const output$ = from(new Promise(res => setTimeout(() => res(1), 500)));
  const initialOutput = 0;
  const output = useObservableState(output$, initialOutput);

  return (
    <>{output}</>
  );
}
export default App;
  • from 将 Promise 转换成了一个 rxjs 流(stream)output$
  • useObservableState 会订阅(subscribe)这个流,流中每一个值 “到来” 的时候,更新 output
  • output 的初始值为 initialOutput,总是先于流中的第一个值被赋值

这种写法非常适合处理异步加载页面的场景:

首屏加载后,前端通过 ajax 请求获取数据,更新页面。

(2)事件更新

功能:页面展示一个按钮和数字 0,每次点击按钮,后面的数字加一。

import { useObservableState } from 'observable-hooks';

function App() {
  const initialOutput = 0;
  const transform = output$ => output$.pipe(
    //
  );
  const [output, onInput] = useObservableState(transform, initialOutput);

  const onClick = () => {
    onInput(output + 1);
  };

  return (
    <>
      <input type="button" value="Add One" onClick={onClick} />
      {output}
    </>
  );
}
export default App;
  • output 的初始值被设置为 initialOutput
  • 每次调用 onInput 就会向 output$ 流中加入一个新值
  • transform 是流变换函数,可以用 rxjs pipe + operators 来实现
  • useObservableState 会订阅 transform 过后的新流,来对 output 重新赋值

每次 onInput 加入流中的值,
通过 transform,再经过 useObservableState,最后赋值给了 output

(3)异步回调

功能:页面加载 500ms 后,写 log

import { from } from 'rxjs';
import { useSubscription } from 'observable-hooks';

function App() {
  const output$ = from(new Promise(res => setTimeout(() => res(1), 500)));
  useSubscription(output$, v => {
    console.log(v);
  });

  return (
    <>Hello World</>
  );
}
export default App;
  • from 会将 Promise 转换成一个流 output$
  • 流中每一个值 “到来” 的时候,触发回调
  • useSubscription 可以多次使用,流中的每一个值会触发多次回调
const output$ = from(new Promise(res => setTimeout(() => res(1), 500)));
useSubscription(output$, v => {
  console.log(v);  // 1
});
useSubscription(output$, v => {
  console.log(v);  // 1
});

实际上,上文介绍的 useObservableStateuseSubscription 对于同一个流也是可以多次使用的。

import { from } from 'rxjs';
import { useObservableState, useSubscription } from 'observable-hooks';

function App() {
  const output$ = from(new Promise(res => setTimeout(() => res(1), 500)));

  const initialOutput = 0;
  const output = useObservableState(output$, initialOutput);
  debugger;  // 1 [先触发,跟先后顺序有关]
  
  useSubscription(output$, v => {
    debugger;  // 1 [后触发,跟先后顺序有关,放在前面就会先触发]
  });

  return (
    <>Hello World</>
  );
}
export default App;

(4)变量写入流

功能:
页面展示一个按钮和数字 0,每次点击按钮,后面的数字加一。
按钮点击会改变组件状态,组件状态变更,会导致流中加入新值。

import { useState } from 'react';
import { useObservableState, useObservable } from 'observable-hooks';

function App() {
  const [state, setState] = useState(0);

  const transform = output$ => output$.pipe(
    //
  );
  const output$ = useObservable(  // 这里是关键
    transform,
    [state]
  );

  const initialOutput = state;
  const output = useObservableState(output$, initialOutput);

  const onClick = () => {
    setState(state + 1);
  };

  return (
    <>
      <input type="button" value="Add One" onClick={onClick} />
      {output}
    </>
  );
}
export default App;
  • useObservable 的第二个参数,传入了 output$ 依赖的变量列表 [state]
    state 值改变时,就会向流中加入这个值,随后 transform 对流进行了变换
  • 组件通过 setState 更新后 output$ 并没有变(是同一个对象),改变的只是流中的值

(5)事件写入流

功能:
页面展示一个按钮和数字 0,每次点击按钮,后面的数字加一。
按钮点击事件,会直接在流中加入新值。

import { useObservableState, useObservableCallback } from 'observable-hooks';

function App() {
  const transform = output$ => output$.pipe(
    //
  );
  const [onInput, output$] = useObservableCallback(  // 这里是关键
    transform
  )

  const initialOutput = 0;
  const output = useObservableState(output$, initialOutput);

  const onClick = () => {
    onInput(output + 1);
  };

  return (
    <>
      <input type="button" value="Add One" onClick={onClick} />
      {output}
    </>
  );
}
export default App;
  • useObservableCallback 会返回一个函数 onInput 和一个流 output$,调用这个函数 onInput 会向 output$ 中加入新值
(6)创建常量流

上文提到了 from 将 Promise 转换成流的做法,每次调用 from 都会产生新的流。
尤其是当组件通过 setState 状态更新的时候。

而使用下述写法,可以创建一个在组件更新时不变的流 output$
每次 useObservable 都返回相同的对象。

import { useState } from 'react';
import { from } from 'rxjs';
import { useObservable } from 'observable-hooks';

let a;
function App() {
  const [state, setState] = useState(0);

  const output$ = useObservable(
    () => from(new Promise(res => setTimeout(() => res(1), 500))),
  );

  const l = a === output$;
  debugger;     // 组件更新时 l === true
  a = output$;  // 保存上一次的值

  const onClick = () => {
    setState(state + 1);
  };

  return (
    <input type="button" value="Add One" onClick={onClick} />
  );
}
export default App;

3. 小结

  • 上文总共介绍了 4 个 hooks,用于了 6 个场景
// 异步取值 [流 -> 值]
const output = useObservableState(output$, initialOutput);

// 事件更新 [流变换 -> 值, 写入流]
const [output, onInput] = useObservableState(transform, initialOutput);

// 异步回调 [流, 回调 -> void]
useSubscription(output$, v => {
  console.log(v);
});

// 变量写入流 [流变换, 变量依赖 -> 常量流]
const output$ = useObservable(
  transform,
  [state],
);

// 事件写入流 [流变换 -> 写入流, 常量流]
const [onInput, output$] = useObservableCallback(
  transform,
);

// 创建同一个流 [流变换 -> 常量流]
const output$ = useObservable(
  () => from(new Promise(res => setTimeout(() => res(1), 500))),
);
  • 以上 hooks 都是建立在 “常量流” 基础之上的,即组件更新后 $output 并没有变,只是流中的值发生了变化。
import { useState } from 'react';
import { from } from 'rxjs';
import { useObservableState } from 'observable-hooks';

function App() {
  const [state, setState] = useState(0);

  const output$ = from(new Promise(res => setTimeout(() => res(1), 500)));
  const output = useObservableState(output$, 0);  // output$ 在组件更新后重置,所以 output 总是为 0

  const onClick = () => {
    setState(state + 1);
  };

  return (
    <>
      {output}
      <input type="button" value="Add One" onClick={onClick} />
    </>
  );
}
export default App;
  • 使用 Observable Hooks 时,我是这么考虑的:
    • 我们需要一个 “常量流”,在组件更新时不变(里面的值会变)
    • 依赖的变量改变了、或事件主动触发,都可以在流中添加值
    • 流中有了新值,会重新触发流变换函数 transform
    • 使用 useObservableState 从流中取值来用

参考

Observable Hooks
RxJS

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354