函数式响应式编程 - Functional Reactive Programming

我们略过概念,直接看函数式响应式编程解决了什么问题。

故事从下面这个例子展开:

两个密码输入框,一个提交按钮。

密码、确认密码都填写并一致,允许提交;不一致提示错误。

HTML 如下:

<input id="pwd" placeholder="输入密码" type="password" /><br />
<input id="confirmPwd" placeholder="再次确认" type="password" />
<label id="errorLabel"></label><br />
<button id="submitBtn" disabled>提交</button>

常规做法

初始版

const validate = () => {
  const match = pwd.value === confirmPwd.value;
  const canSubmit = pwd.value && match;
  errorLabel.innerText = match ? "" : "密码不一致";
  if (canSubmit) {
    submitBtn.removeAttribute("disabled");
  } else {
    submitBtn.setAttribute("disabled", true);
  }
};

pwd.addEventListener("input", validate);
confirmPwd.addEventListener("input", validate);

加强版

问题: 输入密码时,确认密码还是空的,出现密码不一致错误提示,干扰用户输入。

期望: 确认密码没输入过时,不提示错误。

为解决这个问题,用 isConfirmPwdTouched 标识确认密码输入框是否输入过内容。

let isConfirmPwdTouched = false;
pwd.addEventListener("input", () => {
  if (isConfirmPwdTouched) validate();
});
confirmPwd.addEventListener("input", () => {
  isConfirmPwdTouched = true;
  validate();
});

测试同学又发现了一个 bug:
不输密码,直接输入确认密码,这时又出现了错误提示。

为解决这个问题,再加入一个标识位 isPwdTouched

let isConfirmPwdTouched = false;
let isPwdTouched = false;
pwd.addEventListener("input", () => {
  isPwdTouched = true;
  if (isPwdTouched && isConfirmPwdTouched) validate();
});
confirmPwd.addEventListener("input", () => {
  isConfirmPwdTouched = true;
  if (isPwdTouched && isConfirmPwdTouched) validate();
});

旗舰版

问题: 确认密码输入框输入第一个字符时就会提示密码不一致,干扰用户输入。

期望: 连续输入时,不提示错误。

为解决这个问题,高级一点的做法是使用高阶函数 debounce,否则又要多个标识位。

const debounce = (fn, ms) => {
  let timeoutId;
  return (...args) => {
    if (timeoutId !== undefined) clearTimeout(timeoutId);
    timeoutId = setTimeout(fn.bind(null, ...args), ms);
  };
};

const validate = () => {
  const match = pwd.value === confirmPwd.value;
  const canSubmit = pwd.value && match;
  errorLabel.innerText = match ? "" : "密码不一致";
  if (canSubmit) {
    submitBtn.removeAttribute("disabled");
  } else {
    submitBtn.setAttribute("disabled", true);
  }
};

const debouncedValidate = debounce(validate, 200);

let isConfirmPwdTouched = false;
let isPwdTouched = false;
pwd.addEventListener("input", () => {
  isPwdTouched = true;
  if (isPwdTouched && isConfirmPwdTouched) debouncedValidate();
});
confirmPwd.addEventListener("input", () => {
  isConfirmPwdTouched = true;
  if (isPwdTouched && isConfirmPwdTouched) debouncedValidate();
});

常规做法的问题

可以看出:随着交互越来越复杂,常规做法的标识位越来越多,代码的逻辑越来越难理清。

常规做法实际实现了下图的逻辑:

图看起来清晰易懂,但可惜的是 代码和这张图长得并不像。

有没有一种办法,让我们的代码和上图一样逻辑清晰呢?
答案就是:函数式响应式编程。

用它写代码就像是在画上面那张图。


函数式响应式做法

这里使用的库是rxjs

const { fromEvent, combineLatest } = rxjs;
const { map, debounceTime } = rxjs.operators;

const pwd$ = fromEvent(pwd, "input").pipe(map(e => e.target.value));
const confirmPwd$ = fromEvent(confirmPwd, "input").pipe(
  map(e => e.target.value)
);

combineLatest(pwd$, confirmPwd$)
  .pipe(
    debounceTime(200),
    map(([pwd, confirmPwd]) => ({
      match: pwd === confirmPwd,
      canSubmit: pwd && pwd === confirmPwd
    }))
  )
  .subscribe(({ match, canSubmit }) => {
    errorLabel.innerText = match ? "" : "密码不一致";
    if (canSubmit) {
      submitBtn.removeAttribute("disabled");
    } else {
      submitBtn.setAttribute("disabled", true);
    }
  });

没看出代码和上面那张图有什么相似?我们来拆解一下。

const pwd$ = fromEvent(pwd, "input").pipe(map(e => e.target.value));
const confirmPwd$ = fromEvent(confirmPwd, "input").pipe(
  map(e => e.target.value)
);

我们把 pwd$, confirmPwd$ 称作流,可以把它们想象成河流,里面流淌着数据。

map 把流中的 input event 转换为输入框的 value

combineLatest(pwd$, confirmPwd$);

combinLatest 的作用在这里有两个。

  1. combine:把 pwd$, confirmPwd$ 合成一个新流
  2. latest:新流中的数据为 pwd$, confirmPwd$ 最新的数据的组合
    1. pwd$ 产生数据 a 时,confirmPwd$ 还没产生过数据,新流不产生数据;
    2. pwd$ 产生数据 ab 时,confirmPwd$ 还没产生过数据,新流不产生数据;
    3. confirmPwd$ 产生数据 a 时,
      由于 pwd$, confirmPwd$ 都产生过数据了,pwd$ 流最新产生的数据为 ab
      新流产生数据 [ab, a]
    4. confirmPwd$ 产生数据 ab 时,
      由于 pwd$, confirmPwd$ 都产生过数据了,pwd$ 流最新产生的数据为 ab
      新流产生数据 [ab, ab]
combineLatest(pwd$, confirmPwd$).pipe(
  debounceTime(200),
  map(([pwd, confirmPwd]) => ({
    match: pwd === confirmPwd,
    canSubmit: pwd && pwd === confirmPwd
  }))
);

debounceTime(200) 的作用和普通做法里的 debounce 功效一样。

  1. 上游流产生 [ab, a] 时,新流不立刻把数据传给下游,而是要延迟 200ms。
  2. 200ms 不到,上游流又传来数据 [ab, ab],新流丢弃之前的数据。
  3. 200ms 后,上游流没有传来新数据,新流将 [ab, ab] 传给下游。

map[ab, ab] 转化为 { match: true, canSubmit: true }



再比较一下,是不是很像呢?


总结

函数式响应式编程创造的初衷就是解决 listener callback 逻辑表达不直观,代码乱成一团麻 的问题。

至于它为什么叫函数式响应式编程,是因为它的实现借鉴了函数式、响应式编程思想。
例如:

  • declarative
    关注做什么,而不是怎么做。隐藏了很多细节。
  • reactive
    函数时响应式做法,input 输入有变化,button 状态就会跟着变。
    相比较 input 输入变了、再调一遍函数、根据函数输出修改 button 状态,要自动化。
    这句话说的有漏洞,常规做法也很自动化。先跳过吧,以后写一篇响应式编程的文章。
  • ......
  • ......

原文地址:https://www.cnblogs.com/apolis/p/11437688.html

时间: 2024-11-06 17:37:17

函数式响应式编程 - Functional Reactive Programming的相关文章

响应式编程(Reactive programming)

响应式编程是指确保程序对事件或输入做出响应的做法.在这一节,我们将专注于图形界面方面的响应式编程,图形界面总量响应式的.然而,其他网格的编程也需要考虑响应式编程,例如,运行在服务器上的程序总是需要保持对输入作出响应,即使是在它处理其他需要长时间运行的任务期间.我们将在第十一章实现聊天服务器时,会看到在服务器编程方面也要用到这里讨论的一些方法. 大多数图形界面库使用事件循环去处理绘制图形界面,以及与用户之间的交互,就是说,一个线程既要负责绘制图形界面,也要处理其上的所有事件,我们称这种线程为图形界

[转帖]浅谈响应式编程(Reactive Programming)

浅谈响应式编程(Reactive Programming) https://www.jianshu.com/p/1765f658200a 例子写的非常好呢. 0.9312018.02.14 21:22:16字数 1877阅读 9816 这是告别CSDN后第一次使用简书写IT类的博客,还在适应.最不适应的就是不能直接手输markdown语法标记.(好像原因是我没有切换编辑器) 什么是响应式编程(Reactive Programming) In computing, reactive program

响应式编程(Reactive Programming)(Rx)介绍

很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的. 学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下.刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它们都流于表面,从没有围绕响应式编程构建起一个完整的知识体系.库的文档往往也无法帮助你去了解它的函数.不信的话可以看一下这个: 通过合并元素的指针,将每一个可观察的元素序列放射到一个新的可观察的序列中,然后将多个可观察的序列中的一个转换成一个只从最近的可观察序列中产生值得可观察的序列. 天啊. 我看过

高大上函数响应式编程框架ReactiveCocoa学习笔记1 简介

原创文章,转载请声明出处哈. ReactiveCocoa函数响应式编程 一.简介 ReactiveCocoa(其简称为RAC)是函数响应式编程框架.RAC具有函数式编程和响应式编程的特性.它主要吸取了.Net的 Reactive Extensions的设计和实现. 函数式编程 (Functional Programming) 函数式编程也可以写N篇,它是完全不同于OO的编程模式,这里主要讲一下这个框架使用到的函数式思想. 1) 高阶函数:在函数式编程中,把函数当参数来回传递,而这个,说成术语,我

RxJS入门之函数响应式编程

一.函数式编程 1.声明式(Declarativ) 和声明式相对应的编程?式叫做命令式编程(ImperativeProgramming),命令式编程也是最常见的?种编程?式. //命令式编程: function double(arr) { const results = [] for (let i = 0; i < arr.length; i++){ results.push(arr[i] * 2) } return results } function addOne(arr){ const r

Swift 响应式编程 浅析

这里我讲一下响应式编程(Reactive Programming)是如何将异步编程推到一个全新高度的. 异步编程真的很难 大多数有关响应式编程的演讲和文章都是在展示Reactive框架如何好如何惊人,给出一些在非常复杂的情况下,只需几行代码就可以搞定的例子.例子么?我这里有一段基于RxSwift的聊天程序的代码: socket.rx_event .filter({ $0.event == "newMessage" && $0.items?.count > 0})

(1)什么是响应式编程——响应式Spring的道法术器

本系列文章索引:<响应式Spring的道法术器>. 1 响应式编程之道 1.1 什么是响应式编程? 在开始讨论响应式编程(Reactive Programming)之前,先来看一个我们经常使用的一款堪称"响应式典范"的强大的生产力工具--电子表格. 举个简单的例子,某电商网站正在搞促销活动,任何单品都可以参加"满199减40"的活动,而且"满500包邮".吃货小明有选择障碍(当然主要原因还是一个字:穷),他有个习惯,就是先在Excel

响应式编程系列(一):什么是响应式编程?reactor入门

响应式编程 系列文章目录 (一)什么是响应式编程?reactor入门 (二)Flux入门学习:流的概念,特性和基本操作 (三)Flux深入学习:流的高级特性和进阶用法 (四)reactor-core响应式api如何测试和调试? (五)Spring reactive: Spring WebFlux的使用 (六)Spring reactive: webClient的使用 引言 Spring framework 5 的一大新特性:响应式编程(Reactive Programming).那么什么是响应式

函数响应式编程及ReactiveObjC学习笔记 (-)

最近无意间看到一个视频讲的ReactiveObjC, 觉得挺好用的 但听完后只是了解个大概. 在网上找了些文章, 有的写的比较易懂但看完还是没觉得自己能比较好的使用RAC, 有的甚至让我看不下去 这两天刚好公司项目交付闲下来, 想自己去啃下官方文档 ReactiveCocoa是一个基于函数响应式编程的OC框架. 那么什么是函数式响应式编程呢?概念我就不讲了 因为我讲的也不一定准确, 大家可以去baidu看看大神们的解释 下面我大概演示下响应式编程的样子 Masonry是比较常见的一个响应式框架,