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

匿名 (未验证) 提交于 2019-12-02 21:53:52

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

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

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

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

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 状态,要自动化。
    这句话说的有漏洞,常规做法也很自动化。先跳过吧,以后写一篇响应式编程的文章。
  • ......
  • ......
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!