函數式響應式編程 - Functional Reactive Programming


我們略過概念,直接看函數式響應式編程解決了什么問題。從下面這個例子展開:兩個密碼輸入框,一個提交按鈕。

../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 狀態,要更自動化。這個解釋有點牽強,常規做法也很自動化。以后我需要再好好研究下響應式編程。
  • ......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM