JavaScript 中使用高阶流的函数响应式编程 (frp) 是处理代码中复杂的、基于时间的交互的强大方法。这是一种将我们的程序视为一系列数据流,而不是一系列命令式命令的方式。
让我们首先了解什么是流。在 frp 中,流是随时间变化的值序列。它可以是从鼠标点击到 api 响应的任何内容。当我们开始在代码中将这些流视为一等公民时,奇迹就会发生。
高阶流将这个概念更进一步。它们是流的流,使我们能够对更复杂的场景进行建模。想象一下用户搜索流,其中每个搜索都会触发一个新的结果流。这是一个正在运行的高阶流。
我发现掌握这些概念的最佳方法之一是通过实际例子。让我们深入研究一些代码:
const { fromevent } = rxJS; const { map, switchmap } = rxjs.operators; const searchinput = document.getelementbyid('search-input'); const searchbutton = document.getelementbyid('search-button'); const searchstream = fromevent(searchbutton, 'click').pipe( map(() => searchinput.value), switchmap(query => fetchsearchresults(query)) ); searchstream.subscribe(results => { // display results }); function fetchsearchresults(query) { // simulate api call return new promise(resolve => { settimeout(() => { resolve(`results for ${query}`); }, 1000); }); }
在此示例中,我们正在创建搜索查询流。每次单击搜索按钮时,我们都会将单击事件映射到搜索输入的当前值。然后,我们使用 switchmap 为每个搜索查询创建一个新流。
立即学习“Java免费学习笔记(深入)”;
这种方法的美妙之处在于它如何处理快速事件。如果用户快速多次单击搜索按钮,switchmap 将取消任何正在进行的搜索,只向我们提供最新查询的结果。
frp 的主要优点之一是它如何帮助我们管理复杂性。通过从流的角度思考,我们可以将复杂的交互分解为更小、更易于管理的部分。
让我们看另一个例子。假设我们正在构建一个协作文档编辑器。我们想要将更改同步到服务器,但我们不想发送每次击键。我们可以使用 frp 创建去抖动的更改流:
const { fromevent } = rxjs; const { debouncetime, map } = rxjs.operators; const editor = document.getelementbyid('editor'); const changestream = fromevent(editor, 'input').pipe( debouncetime(300), map(event => event.target.value) ); changestream.subscribe(content => { sendtoserver(content); }); function sendtoserver(content) { // simulated server send console.log('sending to server:', content); }
在这里,我们创建一个输入事件流,将它们消除 300 毫秒,然后映射到编辑器的内容。这意味着,只有当用户暂停输入至少 300 毫秒时,我们才会向服务器发送更新。
frp 的挑战之一是管理共享状态。函数范式鼓励我们避免可变状态,但有时我们需要跟踪事物。流为我们提供了一种干净地完成此操作的方法:
const { behaviorsubject } = rxjs; const { scan } = rxjs.operators; const initialstate = { count: 0 }; const state$ = new behaviorsubject(initialstate); const increment$ = new behaviorsubject(1); const decrement$ = new behaviorsubject(-1); const counter$ = state$.pipe( scan((state, change) => ({ count: state.count + change }), initialstate) ); increment$.subscribe(state$); decrement$.subscribe(state$); counter$.subscribe(state => console.log(state.count)); // increment increment$.next(1); // decrement decrement$.next(-1);
在此示例中,我们使用behaviorsubject 来表示我们的应用程序状态。我们为递增和递减操作创建单独的流,然后使用扫描运算符将这些更改累积到新状态。
这种模式为我们提供了不可变状态更新的好处,同时仍然允许我们将应用程序建模为一系列流。
frp 最强大的方面之一是它如何让我们从简单的构建块组成复杂的行为。让我们看一个如何实现拖放功能的示例:
const { fromevent, merge } = rxjs; const { map, takeuntil, switchmap } = rxjs.operators; const draggable = document.getelementbyid('draggable'); const mousedown$ = fromevent(draggable, 'mousedown'); const mousemove$ = fromevent(document, 'mousemove'); const mouseup$ = fromevent(document, 'mouseup'); const drag$ = mousedown$.pipe( switchmap(start => { const startx = start.clientx - draggable.offsetleft; const starty = start.clienty - draggable.offsettop; return mousemove$.pipe( map(move => ({ x: move.clientx - startx, y: move.clienty - starty })), takeuntil(mouseup$) ); }) ); drag$.subscribe(pos => { draggable.style.left = `${pos.x}px`; draggable.style.top = `${pos.y}px`; });
在这里,我们组合多个事件流来创建一个代表拖动操作的高阶流。 switchmap 运算符让我们为每次拖动创建一个新的流,而 takeuntil 确保当用户释放鼠标按钮时我们停止跟踪鼠标移动。
frp 的挑战之一是处理背压 – 当我们的流产生值的速度比我们消耗它们的速度快时会发生什么? rxjs 为此提供了几种策略。让我们看一个使用 buffertime 运算符的示例:
const { interval } = rxjs; const { buffertime } = rxjs.operators; const faststream$ = interval(10); // emits every 10ms const bufferedstream$ = faststream$.pipe( buffertime(1000) // collect values for 1 second ); bufferedstream$.subscribe(buffer => { console.log(`received ${buffer.length} values`); });
在此示例中,我们将快速流中的值缓冲到每秒发出一次的数组中。这对于处理高频事件(例如鼠标移动或传感器读数)非常有用。
随着我们深入研究 frp,我们经常发现自己想要创建自定义运算符。 rxjs 使这变得相对简单:
const { observable } = rxjs; function customoperator() { return (source$) => { return new observable(observer => { return source$.subscribe({ next(value) { if (value % 2 === 0) { observer.next(value * 2); } }, error(err) { observer.error(err); }, complete() { observer.complete(); } }); }); }; } const source$ = of(1, 2, 3, 4, 5); const result$ = source$.pipe(customoperator()); result$.subscribe(x => console.log(x)); // outputs: 4, 8
这个自定义运算符将偶数加倍并过滤掉奇数。创建自定义运算符使我们能够封装复杂的流操作并在我们的应用程序中重用它们。
frp 真正发挥作用的一个领域是处理复杂的异步操作。让我们看一个示例,了解如何实现具有指数退避的重试机制:
const { of, throwerror } = rxjs; const { mergemap, delay, retry } = rxjs.operators; function fetchwithretry(url) { return of(url).pipe( mergemap(u => { // simulate a failing api call return math.random() < 0.5 ? throwerror('api error') : of(`response from ${u}`); }), retry({ count: 3, delay: (error, retrycount) => { const delay = math.pow(2, retrycount) * 1000; console.log(`retrying in ${delay}ms`); return of(null).pipe(delay(delay)); } }) ); } fetchwithretry('https://api.example.com') .subscribe( response => console.log(response), error => console.error('failed after 3 retries', error) );
在此示例中,我们将重试运算符与实现指数退避的自定义延迟函数结合使用。当表达为流时,这种复杂的异步行为变得更易于管理。
当我们使用 frp 构建更大的应用程序时,我们经常需要管理多个相互交互的流。 mergelatest 运算符对此非常有用:
const { combinelatest, behaviorsubject } = rxjs; const userprofile$ = new behaviorsubject({ name: 'john' }); const userpreferences$ = new behaviorsubject({ theme: 'light' }); const currentroute$ = new behaviorsubject('/home'); const appstate$ = combinelatest([ userprofile$, userpreferences$, currentroute$ ]).pipe( map(([profile, preferences, route]) => ({ profile, preferences, route })) ); appstate$.subscribe(state => { console.log('app state updated:', state); }); // update individual streams userpreferences$.next({ theme: 'dark' }); currentroute$.next('/settings');
这种模式允许我们为应用程序状态的不同方面维护单独的流,同时仍然能够对整体状态的变化做出反应。
frp 最强大的方面之一是它如何改变我们思考代码的方式。我们不是命令式地逐步描述我们的程序应该做什么,而是声明式地描述数据流和转换。这通常会导致代码更容易推理和测试。
说到测试,frp 可以让我们的测试更加稳健、不那么脆弱。我们可以直接测试我们的流,而不是依赖复杂的模拟和存根:
const { TestScheduler } = require('rxjs/testing'); describe('My Observable', () => { let testScheduler; beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); }); it('should filter even numbers', () => { testScheduler.run(({ cold, expectObservable }) => { const source$ = cold('a-b-c-d-e-|', { a: 1, b: 2, c: 3, d: 4, e: 5 }); const expected = '---b---d-|'; const result$ = source$.pipe(filter(x => x % 2 === 0)); expectObservable(result$).toBe(expected, { b: 2, d: 4 }); }); }); });
这个例子使用rxjs的testscheduler来测试一个简单的过滤操作。这种方法的优点在于我们可以以同步、确定性的方式测试复杂的异步行为。
正如我们所见,具有高阶流的 frp 提供了一个强大的工具包来管理 javascript 应用程序的复杂性。它使我们能够以声明性方式表达复杂的、基于时间的交互,从而使代码通常更易于维护且更易于推理。
然而,这并不是灵丹妙药。与任何范例一样,frp 也有其学习曲线和潜在陷阱。明智地使用它并了解何时更传统的命令式方法可能更简单非常重要。
随着我们继续构建日益复杂的反应式系统,frp 为我们提供了一套强大的工具和模式。通过以流的方式思考,我们可以创建更具弹性、响应更快且可维护的应用程序。无论我们是处理用户输入、管理应用程序状态,还是编排复杂的异步操作,frp 都使我们能够清晰、简洁地表达我们的意图。
frp 之旅可能充满挑战,但也非常有价值。当我们对这些概念越来越熟悉时,我们会发现自己能够解决曾经看似棘手的问题。我们将编写更具声明性、更具可组合性并且最终更强大的代码。
所以让我们拥抱直播吧。让我们思考流动和转换。让我们构建真正响应式的应用程序,以优雅地响应复杂、不断变化的用户交互和数据流世界。借助 frp 和高阶流,我们拥有了创建下一代响应式、弹性 javascript 应用程序的工具。
我们的创作
一定要看看我们的创作:
投资者中心 | 智能生活 | 时代与回响 | 令人费解的谜团 | 印度教 | 精英开发 | js学校
我们在媒体上
科技考拉洞察 | 时代与回响世界 | 投资者中央媒体 | 令人费解的谜团 | 科学与时代媒介 | 现代印度教