函数式响应式编程 - 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 (isConfirmPwdTouched) validate();
});
confirmPwd.addEventListener("input", () => {
isConfirmPwdTouched = true;
if (isPwdTouched) validate();
});
问题: 确认密码输入框输入第一个字符时就会提示密码不一致,干扰用户输入。
期望: 连续输入时,不提示错误。
为解决这个问题,高级一点的做法是使用高阶函数 debounce
,否则又要多个标识位。
const debounce = (fn, ms) => {
let timeoutId;
return (...args) => {
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
作用有两个:
- 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 状态,要更自动化。这个解释有点牵强,常规做法也很自动化。以后我需要再好好研究下响应式编程。 - ......
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)