函数式响应式编程 - 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
的作用在这里有两个。
- combine:把
pwd$
,confirmpwd$
合成一个新流 - latest:新流中的数据为
pwd$
,confirmpwd$
最新的数据的组合-
pwd$
产生数据a
时,confirmpwd$
还没产生过数据,新流不产生数据; -
pwd$
产生数据ab
时,confirmpwd$
还没产生过数据,新流不产生数据; -
confirmpwd$
产生数据a
时,
由于pwd$
,confirmpwd$
都产生过数据了,pwd$
流最新产生的数据为ab
,
新流产生数据[ab, a]
; -
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
功效一样。
- 上游流产生
[ab, a]
时,新流不立刻把数据传给下游,而是要延迟 200ms。 - 200ms 不到,上游流又传来数据
[ab, ab]
,新流丢弃之前的数据。 - 200ms 后,上游流没有传来新数据,新流将
[ab, ab]
传给下游。
map
将 [ab, ab]
转化为 { match: true, cansubmit: true }
。
再比较一下,是不是很像呢?
总结
函数式响应式编程创造的初衷就是解决 listener
callback
逻辑表达不直观,代码乱成一团麻 的问题。
至于它为什么叫函数式响应式编程,是因为它的实现借鉴了函数式、响应式编程思想。
例如:
-
declarative
关注做什么,而不是怎么做。隐藏了很多细节。 -
reactive
函数式响应式做法,input 输入有变化,button 状态就会跟着变。
相比较 input 输入变了、再调一遍函数、根据函数输出修改 button 状态,要自动化。
这句话说的有漏洞,常规做法也很自动化。先跳过吧,以后写一篇响应式编程的文章。 - ......
- ......
推荐阅读
-
JavaScript函数式编程(Functional Programming)组合函数(Composition)用法分析
-
JavaScript函数式编程(Functional Programming)箭头函数(Arrow functions)用法分析
-
JavaScript函数式编程(Functional Programming)纯函数用法分析
-
JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法分析
-
Reactive programming反应式编程介绍
-
Reactive programming反应式编程介绍
-
函数式响应式编程 - Functional Reactive Programming
-
函数式编程(Functional Programming)
-
函数式编程(Functional Programming)
-
响应式编程(Reactive Programming)介绍