在第一個 await 之前呼叫 event.preventDefault(),執行非同步驗證,然後使用 form.requestSubmit() 開始新的提交嘗試。一次性防護機制可讓第二次嘗試繼續進行,而不會重複非同步檢查。
本教學適合在 Mac 上編輯小型瀏覽器介面的人。你將建立一個本機 HTML 範例,其中表單只有在合成標籤檢查通過後才會關閉對話框。同一種事件處理模式也適用於會導覽或傳送請求的一般表單。
為什麼非同步提交處理常式不會延遲提交?
瀏覽器不會等待事件監聽器傳回的 promise。DOM 事件派送演算法會呼叫監聽器並繼續執行;HTML 提交演算法會先檢查 submit 事件是否已取消,再決定是否繼續。將 async 加到監聽器不會在這個生命週期中加入等待步驟。DOM 標準:事件派送、HTML 標準:表單提交演算法
非同步函式會同步執行,直到遇到第一個 await,然後才以非同步方式繼續。因此,即使等待的 promise 已經 fulfilled,也必須在第一個 await 之前取消事件。之後再呼叫 preventDefault(),無法撤銷瀏覽器已做出的提交決定。MDN:非同步函式
取消會結束原本的嘗試,不會暫停它以便稍後恢復。驗證成功後,程式碼必須明確要求再次提交。此外,preventDefault() 不會停止事件傳播,因此其他監聽器仍可觀察到該事件。MDN:Event.preventDefault()
請監聽表單的 submit 事件。這涵蓋透過按鈕提交、支援的隱式提交方式(例如按下 Enter),以及呼叫 requestSubmit() 的情況。只處理按鈕點擊的監聽器無法涵蓋所有入口。原生限制驗證通常會先執行:必填欄位無效時,會阻止提交,也不會觸發 submit。MDN:HTMLFormElement submit 事件
哪個方法能保留表單的提交行為?
如果檢查成功後應回到正常的表單生命週期,請使用 requestSubmit()。直接呼叫 submit() 會略過限制驗證和 submit 事件。這種捷徑會改變非同步處理常式原本應保留的行為。MDN:HTMLFormElement.submit()
| 做法 | 會發生什麼事 | 在此處是否適用 |
|---|---|---|
| 從監聽器傳回 promise | 事件派送會繼續,不會等待 promise | 不會延遲預設動作 |
取消事件,然後呼叫 submit() |
提交會略過原生驗證和 submit 監聽器 | 會改變生命週期 |
取消事件,然後呼叫 requestSubmit(submitter) |
原生驗證和 submit 事件會再次執行 | 保留預期的提交流程 |
等待前先擷取 event.submitter。將該按鈕傳給 requestSubmit(),即可保留按鈕的作用,包括提交覆寫設定,以及適用時對 name/value 的貢獻。如果沒有提交按鈕,請不帶引數呼叫 requestSubmit()。提供的按鈕仍須屬於該表單;無效按鈕或屬於其他表單的按鈕會引發例外。MDN:HTMLFormElement.requestSubmit()
新的提交會再次觸發監聽器。因此,下方範例使用兩個不同的狀態:pending 可避免非同步檢查重疊執行,而 allowNextSubmit 則只允許處理常式同步通過一次。永久性的「已驗證」旗標範圍太廣,因為之後的編輯可能會沿用先前的驗證結果。
在 Mac 上建立完整的本機範例
在純文字程式碼編輯器中,將以下完整文件儲存為 async-form.html,然後在 Mac 上用新版瀏覽器開啟。不需要程式庫、匯入、伺服器或安裝。標準文件列出 Safari 16 及更新版本支援 requestSubmit()。HTML 標準:表單 API
這個範例使用 method="dialog",讓你能在本機清楚看到預設動作。提交成功後會關閉外層對話框,並將對話框的 returnValue 設為提交按鈕的值;不會傳送表單控制項。開啟按鈕會呼叫 showModal() 來顯示對話框。MDN:dialog 元素、MDN:HTMLDialogElement.showModal()
驗證器刻意採用合成規則:短暫計時器結束後,接受去除前後空白後至少有三個字元的標籤、拒絕完全相同的小寫標籤 taken,並在標籤完全等於小寫 error 時擲出例外。這段延遲用來示範非同步完成,不是效能測量。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Asynchronous form validation</title>
</head>
<body>
<h1>Label preview</h1>
<button id="open" type="button">Open form</button>
<p id="outcome" role="status"></p>
<dialog id="editor" aria-labelledby="dialog-title">
<form id="label-form" method="dialog">
<h2 id="dialog-title">Choose a label</h2>
<label for="label">Label</label>
<input id="label" name="label" required autofocus>
<button type="submit" value="accepted">Continue</button>
<button id="cancel" type="button">Cancel</button>
<p id="status" role="status"></p>
</form>
</dialog>
<script>
const dialog = document.getElementById("editor");
const form = document.getElementById("label-form");
const input = document.getElementById("label");
const status = document.getElementById("status");
const outcome = document.getElementById("outcome");
const openButton = document.getElementById("open");
const cancelButton = document.getElementById("cancel");
let pending = false;
let allowNextSubmit = false;
let revision = 0;
async function validateLabel(value) {
await new Promise(resolve => setTimeout(resolve, 700));
const label = value.trim();
if (label === "error") {
throw new Error("Synthetic validation failure");
}
return label.length >= 3 && label !== "taken";
}
openButton.addEventListener("click", () => {
revision += 1;
dialog.returnValue = "";
status.textContent = "";
outcome.textContent = "";
dialog.showModal();
});
input.addEventListener("input", () => {
revision += 1;
});
cancelButton.addEventListener("click", () => dialog.close());
dialog.addEventListener("close", () => {
revision += 1;
outcome.textContent = "Dialog closed. Return value: " +
(dialog.returnValue || "(empty)");
});
form.addEventListener("submit", async event => {
if (allowNextSubmit) {
allowNextSubmit = false;
return;
}
event.preventDefault();
if (pending) return;
pending = true;
const submitter = event.submitter;
const value = input.value;
const checkedRevision = revision;
status.textContent = "Checking label...";
try {
const valid = await validateLabel(value);
if (!dialog.open) return;
if (revision !== checkedRevision || input.value !== value) {
status.textContent = "The label changed. Submit again.";
return;
}
if (!valid) {
status.textContent =
"Use at least three characters and avoid taken.";
return;
}
status.textContent = "Check passed. Submission requested.";
allowNextSubmit = true;
try {
if (submitter) form.requestSubmit(submitter);
else form.requestSubmit();
} finally {
allowNextSubmit = false;
}
} catch (error) {
if (dialog.open && revision === checkedRevision) {
status.textContent = "The check failed. Try again.";
}
console.error(error);
} finally {
pending = false;
}
});
</script>
</body>
</html>
此範例的瀏覽器行為是根據文件所述的生命週期推導而來。小型 Node.js EventTarget 檢查確認了時間上的差異:同步取消會改變派送結果,而在 await 之後取消則趕不上該結果。這裡沒有在 Mac 瀏覽器中執行完整的對話框範例。
防護機制如何保留順序並處理變更?
第一次提交時,處理常式會立即取消事件,並擷取標籤和發起操作的按鈕。檢查尚未完成時,其他 submit 事件也會被取消,但不會因此啟動另一個驗證。這可避免一連串快速提交造成多個檢查重疊執行。
檢查完成後,處理常式會確認對話框仍開啟,而且標籤未曾變更。修訂計數器能偵測使用者編輯,即使使用者改了文字後又恢復原狀也一樣。值比較也會捕捉直接變更值、但欄位文字不同的情況。
檢查通過後,會立即設定 allowNextSubmit,接著呼叫 requestSubmit()。這個方法會再次執行原生驗證和 submit 事件流程。監聽器再次進入時,會取用這次許可並直接返回,不取消事件。接著瀏覽器便能執行預設的關閉對話框動作。這個順序符合標準規定的先驗證、再觸發 submit 事件的流程。HTML 標準:表單提交演算法
內層的 finally 會在沒有發生 submit 事件或方法擲出例外時清除許可。外層的 finally 會在拒絕、例外、結果過時或重試成功後清除 pending。因此,嘗試未成功時,表單仍可再次檢查。
關閉對話框時也會遞增修訂版本。先前開啟時取得的結果,不能授權後續再次開啟時的提交。就這份獨立文件而言,這些檢查已涵蓋可用的互動路徑。如果加入更多欄位或會更新欄位的指令碼,請擴充快照和修訂版本處理,納入驗證器所依賴的每個值。
測試成功、拒絕和中斷的情況
在本機瀏覽器範例中執行以下檢查。預期結果是根據提供的程式碼推論而來,並非宣稱已取得瀏覽器測試結果。
| 操作 | 預期結果 |
|---|---|
將標籤留空並選擇 Continue |
原生必填欄位驗證會在非同步處理常式執行前阻止提交 |
輸入 ab 並選擇 Continue |
檢查完成後,對話框仍開啟並顯示拒絕訊息 |
輸入 taken |
合成規則會拒絕該值 |
輸入 sample |
檢查期間對話框保持開啟,之後關閉並以 accepted 作為回傳值 |
輸入 error |
會顯示失敗訊息,且仍可再次提交 |
| 提交標籤後,在檢查期間編輯它 | 結果會被丟棄,表單會要求再次提交 |
| 檢查期間重複提交 | 只有第一次嘗試會啟動非同步檢查 |
檢查期間選擇 Cancel |
對話框會立即關閉,不會等待待處理的結果 |
也可以在輸入欄位取得焦點時按 Enter 提交。隱式提交取決於表單結構和平台慣例;處理 submit 的用意,是涵蓋實際觸發表單提交的任何受支援互動方式。MDN:HTMLFormElement submit 事件
表單改為傳送一般請求時會有什麼變化?
若是現有會傳送請求的表單,請保留其實際的 action、method、欄位名稱、按鈕值和提交覆寫設定。將合成驗證器換成實際的非同步檢查。開啟和關閉對話框的程式碼只適用於此示範;取消、驗證、要求提交的流程則應放在表單的提交處理常式中。
請讓欄位保持可用,以便最後提交。等待期間停用控制項需要特別留意:瀏覽器建立提交項目清單時會排除停用欄位。範例讓使用者仍可編輯,並丟棄過時的驗證結果。HTML 標準:建立項目清單
如果還有其他 submit 監聽器,它們會觀察到第一次已取消的嘗試,以及後續嘗試。請協調這些監聽器,避免它們重複執行相同工作,或在授權通過時變更已驗證的值。requestSubmit() 也不保證提交完成:其他監聽器可能會取消提交,原生限制也可能阻止提交。它的回傳值是 undefined,不是完成 promise。MDN:HTMLFormElement.requestSubmit()
核心規則很明確:同步取消事件、驗證一組明確定義的值、丟棄過時結果,並授權一次新的原生提交嘗試。這讓非同步驗證在工作流程中有明確的位置,同時保留瀏覽器的表單生命週期。
來源
DOM 標準 — WHATWG
HTML 標準:表單提交演算法 — WHATWG
HTML 標準:表單元素 — WHATWG
HTML 標準:建立項目清單 — WHATWG
非同步函式 — MDN Web Docs
Event:preventDefault() 方法 — MDN Web Docs
HTMLFormElement:submit 事件 — MDN Web Docs
HTMLFormElement:submit() 方法 — MDN Web Docs
HTMLFormElement:requestSubmit() 方法 — MDN Web Docs
Dialog 元素 — MDN Web Docs
HTMLDialogElement:showModal() 方法 — MDN Web Docs