我們略過概念,直接看函數式響應式編程解決了什么問題。從下面這個例子展開:兩個密碼輸入框,一個提交按鈕。
密碼、確認密碼都填寫並一致,允許提交;不一致提示錯誤。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();
});
常規做法的問題
可以看出:隨着交互越來越復雜,常規做法的標識位越來越多,代碼邏輯越來越難理清。
常規做法實際實現了下圖的邏輯:
圖看起來清晰易懂,但很可惜:代碼和這張圖長得並不像。有沒有一種辦法,讓代碼和上面那張圖一樣清晰易懂呢?
答案就是:函數式響應式編程。用它寫代碼就像是在畫上面那張圖。
函數式響應式做法
這里使用的庫是 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 狀態,要更自動化。這個解釋有點牽強,常規做法也很自動化。以后我需要再好好研究下響應式編程。 - ......