zoukankan      html  css  js  c++  java
  • 函数式响应式编程

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

    ../images/frp-demo.png

    密码、确认密码都填写并一致,允许提交;不一致提示错误。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();
    });
    

    常规做法的问题

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

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

    ../images/frp-a.png

    图看起来清晰易懂,但很可惜:代码和这张图长得并不像。有没有一种办法,让代码和上面那张图一样清晰易懂呢?

    答案就是:函数式响应式编程。用它写代码就像是在画上面那张图。


    函数式响应式做法

    这里使用的库是 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));
    

    ../images/frp-b1.png

    我们把 pwd$, confirmPwd$ 称作流,可以把它们想象成河流,里面流淌着数据。map 把流中的 input event 转换为输入框的 value

    combineLatest(pwd$, confirmPwd$);
    

    ../images/frp-b2.png

    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
      }))
    );
    

    ../images/frp-b3.png

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

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

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


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

    ../images/frp-a.png

    ../images/frp-b3.png


    总结

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

    • declarative
      关注做什么,而不是怎么做。隐藏了很多细节。
    • reactive
      函数式响应式做法,input 输入有变化,button 状态就会跟着变。相比较 input 输入变了、再调一遍函数、根据函数输出修改 button 状态,要更自动化。这个解释有点牵强,常规做法也很自动化。以后我需要再好好研究下响应式编程。
    • ......
  • 相关阅读:
    基于Dubbo的压测调优实例
    Rsync同步工具安装文档
    Codeforces 114A-Cifera(暴力)
    UVa 872
    Highcharts可拖动式图表
    Android中配置JDK和SDK的环境变量
    用CSS border相关属性画三角形
    屌丝程序猿赚钱之道 之APP
    软件开发工具(一)——概论
    建造者模式(屌丝专用)
  • 原文地址:https://www.cnblogs.com/apolis/p/11437688.html
Copyright © 2011-2022 走看看