我們略過概念,直接看函數式響應式編程解決了什麼問題。 故事從下麵這個例子展開: 兩個密碼輸入框,一個提交按鈕。 密碼、確認密碼都填寫並一致,允許提交;不一致提示錯誤。 HTML 如下: 常規做法 初始版 加強版 問題: 輸入密碼時,確認密碼還是空的,出現密碼不一致錯誤提示,干擾用戶輸入。 期望: 確 ...
我們略過概念,直接看函數式響應式編程解決了什麼問題。
故事從下麵這個例子展開:
兩個密碼輸入框,一個提交按鈕。
密碼、確認密碼都填寫並一致,允許提交;不一致提示錯誤。
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
的作用在這裡有兩個。
- 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 狀態,要自動化。
這句話說的有漏洞,常規做法也很自動化。先跳過吧,以後寫一篇響應式編程的文章。 - ......
- ......