我们略过概念,直接看函数式响应式编程解决了什么问题。从下面这个例子展开:两个密码输入框,一个提交按钮。

密码、确认密码都填写并一致,允许提交;不一致提示错误。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 (isConfirmPwdTouched) validate();
});
confirmPwd.addEventListener("input", () => {
isConfirmPwdTouched = true;
if (isPwdTouched) 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 (isConfirmPwdTouched) debouncedValidate();
});
confirmPwd.addEventListener("input", () => {
isConfirmPwdTouched = true;
if (isPwdTouched) 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 }


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


总结

函数式响应式编程初衷是为了解决 listenercallback 逻辑表达不直观,代码乱成一团麻 的问题。至于它为什么叫函数式响应式编程,是因为它借鉴了函数式、响应式编程思想。例如:

  • declarative

    关注做什么,而不是怎么做。隐藏了很多细节。
  • reactive

    函数式响应式做法,input 输入有变化,button 状态就会跟着变。相比较 input 输入变了、再调一遍函数、根据函数输出修改 button 状态,要更自动化。这个解释有点牵强,常规做法也很自动化。以后我需要再好好研究下响应式编程。
  • ......

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

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

    浅谈响应式编程(Reactive Programming) https://www.jianshu.com/p/1765f658200a 例子写的非常好呢. 0.9312018.02.14 21:22 ...

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

    很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的. 学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下.刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它 ...

  3. 函数响应式编程(FRP)—基础概念篇

    原文出处:http://ios.jobbole.com/86815/. 一函数响应式编程 说到函数响应式编程,就不得不提到函数式编程,他们俩有什么关系呢?今天我们就详细的解析一下他们的关系. 现在下面 ...

  4. 函数响应式编程(FRP)从入门到”放弃”——基础概念篇

    前言 研究ReactiveCocoa一段时间了,是时候总结一下学到的一些知识了. 一.函数响应式编程 说道函数响应式编程,就不得不提到函数式编程,它们俩到底有什么关系呢?今天我们就详细的解析一下他们的 ...

  5. 从0开发3D引擎(六):函数式反应式编程及其在引擎中的应用

    目录 上一篇博文 介绍函数式反应式编程 函数式反应式编程学习资料 函数式反应式编程的优点与缺点 优点 缺点 异步处理的其它方法 为什么使用Most库 引擎中相关的函数式反应式编程知识点 参考资料 大家 ...

  6. 函数响应式编程(FRP)框架--ReactiveCocoa

    由于工作原因,有段时间没更新博客了,甚是抱歉,只是,从今天開始我又活跃起来了,哈哈,于是决定每周更新一博.大家互相学习.交流. 今天呢.讨论一下关于ReactiveCocoa,这个採用函数响应式编程( ...

  7. RxJS入门之函数响应式编程

    一.函数式编程 1.声明式(Declarativ) 和声明式相对应的编程⽅式叫做命令式编程(ImperativeProgramming),命令式编程也是最常见的⼀种编程⽅式. //命令式编程: fun ...

  8. Swift 响应式编程 浅析

    这里我讲一下响应式编程(Reactive Programming)是如何将异步编程推到一个全新高度的. 异步编程真的很难 大多数有关响应式编程的演讲和文章都是在展示Reactive框架如何好如何惊人, ...

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

    响应式编程 系列文章目录 (一)什么是响应式编程?reactor入门 (二)Flux入门学习:流的概念,特性和基本操作 (三)Flux深入学习:流的高级特性和进阶用法 (四)reactor-core响 ...

随机推荐

  1. 继上篇-jquery ajax提交 本篇用ajax提交的数据去数据库查询

    上篇讲到如何用jquery ajax提交数据至后台,后台接收并返回给ajax.https://www.cnblogs.com/tiezhuxiong/p/11943328.html 今天我们把数据传到 ...

  2. 2019-9-26:渗透测试,基础学习,nmap扫描kali虚拟机服务

    初识Nmap 1, 首先确定kali的ip地址,输入命令ifconfig 2, 开启所需要扫描的服务, 开启ssh:service ssh start, 确认ssh服务是否开启service ssh ...

  3. 网页解析之BeautifulSoup

    介绍及安装 Beautiful Soup 是一个HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据. BeautifulSoup 用来解析 HTML 比较简单,API非常人 ...

  4. STM32F103C8T6 在VSCode下使用Platform IO开发,基于库函数V3.5版本

    首先安装Platform IO插件,怎么安装的教程有很多,可以自行百度,就不在重复了. 本篇文章将会以正点原子的跑马灯例程作为移植对象,基于ST固件库3.5版本 将实现在VSCode上的程序编写与烧录 ...

  5. yum运行报错:libcurl.so.4: cannot open shared object file: No such file or directory

    /usr/lib64/目录下存在libcurl.so.4文件 CURL的动态库找不到,这里我们加入到ld.so.conf [root@localhost bin]#  vim /etc/ld.so.c ...

  6. php 开山篇

    由韩顺平老师讲解的 php课程体系 初级课程只能是静态页面开发,不能动态的使用,只是一个界面 学完之后脑海中 应该有的体系~

  7. java面试常见题目

    JAVA相关基础知识面向对象的特征有哪些方面 1.抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面.抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用 ...

  8. 超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下

    如果你经常遇到 Java 线上性能问题束手无策,看着线上服务 CPU 飙升一筹莫展,发现内存不断泄露满脸茫然.别慌,这里有一款低开销.自带火焰图.让你大呼好用的 Java 性能分析工具 - async ...

  9. Numpy的基础用法

    1.用Numpy创建数组 numpy.array(object):创建数组,与array.array(typecode[, initializer])不同,array.array()只能创建一维数组 ...

  10. Oracle基本的增删改查语句--本人使用scott用户中的表

    --感觉有用点个赞^v^ 1 --创建表空间 create tablespace mykebai datafile 'c:\mykebai.dbf' --数据问价存放位置 size 100m --数据 ...