CSCI2720 · 階段模擬 A
複習路線
本練習涵蓋由瀏覽器/伺服器基礎至 React 元件的七個教學單元。先沿以下路線複習,再作答試題,完成後才閱讀詳解與評分指引。建議用時90分鐘及100分的分配,均為這套階段練習的學習安排。
1.1 瀏覽器、伺服器與網頁資源
先辨認資源及負責處理它的程式。HTTP 請求指出操作與資源;回應則提供狀態、標頭及資源的表示內容。一個可見網頁可以組合 HTML 文件、樣式表、指令碼及圖片,而這些資源可來自不同請求。計算請求數量前,須先列明快取、重新導向及額外相依資源等假設。請求數量本身並不能確定資源嚴格逐一載入的次序。
應能分辨 URL 的通訊協定、主機、連接埠、路徑、查詢及片段。解析相對 URL 時,應以文件的基底 URL 為依據,而非直接串接字串。來源(origin)包括通訊協定、主機與連接埠。片段可指定文件中的位置,但不屬於 HTTP 請求目標的一部分。
分析按鈕互動時,列出事件發生前的狀態、處理函式的操作,以及其後的 DOM 變化。畫面改變本身不代表曾發出網絡請求,亦不代表伺服器已儲存某個值。若每份新文件都重新初始化某變數,載入另一份文件實例時,該變數便重新開始,除非程式另有使用持久儲存機制。
與其背誦欄位名稱,不如完整解析一個位址。以 https://study.example.test/unit3/page.html 為基底,參照 ../icons/book.svg?scale=2#key 先使用目錄 /unit3/,再上移一層至 /,然後附上 icons/book.svg,所以路徑是 /icons/book.svg。把 ?scale=2 保留為查詢,把 #key 保留為片段。來源是 https://study.example.test,一般請求目標則是 /icons/book.svg?scale=2。這樣分開,便能回答兩個不同的診斷問題:請求哪個資源,以及客戶端會解讀哪個片段。查詢文字 scale=2 本身不會建立 JavaScript 數值,亦不能證明伺服器曾儲存任何資料。
準備度自檢: 給定文件 URL、相對圖片 URL 及點擊處理函式,你能推導完整資源網址,並分辨請求目標與片段嗎?你能準確說明哪些值會保留至下一次點擊,哪些會保留至下一份文件嗎?
2.1 語義化 HTML 文件
把原始碼讀成關係的描述。元素有其角色;標籤界定它在原始碼中的範圍;屬性則提供目的地、識別碼或類別等值。應分辨文件標題與畫面上的標題、導覽區與一般文字集合,以及資料表格與純粹的視覺排列。類別名稱本身既不會產生樣式,也不會產生行為。
閱讀表格時,辨認表格標題、列標頭、欄標頭,以及每個資料儲存格與這些標頭的關係。圖片方面,應分辨有資訊作用的替代文字與裝飾圖片所用的空替代文字。顯示原始碼文字時,須適當編碼 < 與 &,不能假定瀏覽器會原樣顯示標記。字元參照在 DOM 中成為字元;它不會觸發第二次解析,把產生的字元序列重新建成元素。
假設某儲存格含 40,其列標頭是 DOM,欄標頭是 Minutes。同時沿這兩種關係閱讀,意思便是一個40分鐘的 DOM 學習時段。若缺少欄標頭,40也可能代表頁數或分數。表格賦予這個值意義,但不會把儲存格的 DOM 文字、即字串 "40",變成應用程式數值。若程式要把兩個時段的時間相加,便應讀取文字並明確進行所需轉換。選擇標題與表格標頭,是為了在視覺佈局改變後仍保留這些關係,而不只是因為某標籤的預設字體看起來合用。
準備度自檢: 你能根據意義選擇元素、利用標頭解釋某儲存格的意義,並分辨字面文字 <em>focus</em> 與真正的 em 元素嗎?
3.1 響應式 CSS 與 Bootstrap
先找出哪些宣告適用於元素。當規則同屬作者來源、沒有層級且均為一般宣告時,先比較選擇器優先度(specificity),再比較原始碼次序。直接套用於元素的宣告與繼承所得的值並不相同。把規則移到外部樣式表或 style 元素,本身不會改變其優先度或作者來源。預測勝出規則時,須說明這些條件。
畫出內容、內距、邊框與外距各層。使用 content-box 時,把左右內距與邊框加到宣告寬度,便得到邊框盒寬度。使用 border-box 時,從宣告寬度扣除這些部分,便得到內容寬度。外距在兩種宣告寬度之外。計算正常文件流時,不要把變形效果當作額外分配了版面空間。
分析響應式規則時,先判斷實際視口條件,並保留同一份底層內容。Bootstrap 預設網格的一列分為12個寬度單位。在各指定視口寬度,辨認當時生效的斷點類別:較高層級的類別由其最小寬度起生效,直至另一適用宣告改變結果。靜態網格類別需要樣式表;可互動的摺疊或下拉功能則另外需要相關 JavaScript。
用具體數字計算:某卡片的宣告寬度為180個 CSS 像素,左右各有8像素內距及2像素邊框。使用 content-box 時,內容寬度是180,邊框盒則是 180 + 2×8 + 2×2 = 200 個 CSS 像素。使用 border-box 時,整個邊框盒是180,內容則是 180 - 16 - 4 = 160。加減前先畫出左右兩側;若只加一次內距,計算的便是另一個幾何結構。先確定實際生效的 box-sizing 宣告,再辨認已知與所求各是哪一種寬度。知道內容寬度,不代表可以用 content-box 取代實際規則;它只告訴你要換算哪個量。視口門檻比較的是視口的 CSS 像素寬度,不是這張卡片的寬度;網格比例描述的是所屬列的幾分之幾,而不是某個以像素表示的長度。
準備度自檢: 你能證明某顏色為何勝出、計算兩種盒模型,並說明指定斷點前後的排列,而不把 HTML 類別的排列次序誤當作 CSS 優先次序嗎?
4.1 JavaScript、函式與集合
依求值次序追蹤運算式,並保留每個中間值的型別。數值加法與字串串接是不同操作。若輸入預期用作數值,應明確轉換。非空字串如 "0" 在條件判斷中為真值,而數值零為假值;嚴格相等不會先把數值轉成字串。
分清函式建立、引數傳入、呼叫及傳回。回呼函式可以同步執行。閉包保留對建立時所屬外圍綁定的存取;不同呼叫所建立的閉包,可以各自保留獨立狀態。缺少傳回值會得到 undefined;若箭頭函式使用區塊主體,而目的是提供結果,便須明寫 return。
分析陣列時,除值的列表外,亦畫出參照關係。賦值可建立別名。展開語法或 slice 複本會建立另一個外層陣列,但其中巢狀物件的參照仍可共用。分清傳回值與原地修改:map、filter 建立陣列,forEach 不收集回呼的傳回值,而 splice 修改其接收者。產生器的 next() 同時傳回值與完成旗標;最後的 return 並不是另一個 yield 項目。
追蹤閉包時,為每次工廠函式呼叫畫一個狀態格。若計數器由總值10建立,其傳回函式加4後變為14,下一次呼叫加−1後變為13。第二次呼叫從保留的14開始,不是從原本的10開始。另一個同樣由10建立的計數器,則在它自己的函式執行前仍保留10。因此閉包可以維持彼此獨立的事件處理狀態。同時也要記錄每步的運算子與型別:把數值2加到字串 "6" 會得到 "62",把它加到已轉換的數值6則得到8。輸入字元看似相同,不代表進行的操作相同。
準備度自檢: 你能預測混合字串與數值的運算式、追蹤獨立閉包,並解釋為何透過淺層複本修改巢狀物件會反映於原陣列,而附加外層成員卻不會嗎?
4.2 DOM 事件與物件狀態
從初始樹狀結構開始,辨認每個 API 傳回哪些物件。querySelectorAll 記錄固定的成員,但當中的節點仍可修改。即時集合則隨文件中的成員變化而更新。附加一個已有節點會把它移動,不會建立第二個獨立節點。
分析事件時,分辨事件起點 target 與監聽器所在的 currentTarget,並按照題目指定的傳播階段與註冊次序追蹤監聽器。表單的預設提交與點擊事件冒泡至祖先監聽器,是不同的行為。事件處理函式只會透過它實際執行的操作改變應用程式狀態。
根據意義選擇更新方式:textContent 提供文字,value 讀取或設定控制項的目前值,classList 修改類別 token。解釋方法呼叫時,應辨認其接收者,而非假定把函式移到別處仍會保留同一個 this。JSON 是適合以此格式表示的資料文字;解析會建立有新物件身分的值,不會還原方法。
假設初始有三個匹配的列表節點,同時記錄靜態選擇器結果與即時集合。把第一個節點的標籤由 Draft 改成 Ready,再附加第四個匹配節點。靜態結果仍含三個節點參照,但其第一個節點現在顯示 Ready;即時集合則含四個成員。靜態結果固定的是成員,不是各節點舊屬性的照片。若操作應針對原先選定的節點,便選擇固定結果;若目的是觀察目前匹配的成員,便選擇即時集合。若在迭代即時集合期間修改樹狀結構,須考慮成員可能在迭代途中改變,不能假定各索引仍描述原本那批節點。
準備度自檢: 你能追蹤巢狀元素的點擊、列出靜態與即時集合的目前長度,並分辨物件別名與獨立解析所得的 JSON 值嗎?
5.1 表單、驗證與 Fetch
建立提交項目前,先列出符合提交條件且有名稱的控制項。停用的控制項會被略去;有名稱的唯讀控制項則可提供其值。未勾選的核取方塊不會產生項目。畫面上的標籤與 id 都不能取代提交所用的 name。
分辨控制項原始字串值、應用程式預期的值,以及明確轉換後所得的數值。required 不能表達每一項應用程式規則:空格可能符合文字控制項的非空要求,卻不符合必須含可見字元的規則。自訂有效性訊息會持續生效,直至程式把它清除。介面限制亦不能取代接收資料邊界的驗證。
把非同步工作分階段追蹤:取得回應、檢查狀態、讀取回應主體,以及解讀所得資料。HTTP 錯誤回應仍可使 Fetch 操作 fulfilled;即使狀態成功,無效 JSON 亦可使讀取主體的操作 rejected。await 前的工作在當前呼叫中開始,其後的續行稍後才執行。若讀取依賴某次寫入,應先等候該寫入完成。
把值的追蹤與有效性的追蹤分開。數值輸入可持有原始字串 "6";檢查相關輸入限制後,明確轉換為數值便得到6,因此再加兩頁得到8,而不是串接文字 "62"。現在假設程式設下自訂錯誤訊息。把控制項改成另一個可接受數值,只改變其值;自訂訊息仍是另一項有效性狀態,直至程式清除它。請求之後亦須作相同區分:可用回應提供狀態,該狀態分辨 HTTP 成功與錯誤,讀取主體則提供資料,讓程式依應用契約解讀。即使是錯誤回應,也可帶有有用的診斷資料。不要只因回應已可用,就刪去學習者輸入的內容。
準備度自檢: 你能分別預測表單項目與有效性、找出請求追蹤中失敗的階段,並解釋記憶體服務回應所保證的狀態壽命嗎?
6.1 元件、狀態與渲染
把元件視為其目前 props 與狀態的畫面描述。渲染計算介面;事件處理函式要求更新狀態;其後的提交才改變 DOM。執行中的處理函式所捕捉的狀態變數,仍是該次渲染的快照。重複以該快照計算取代值,與接收前一待處理值的 updater 函式,會產生不同結果。
受控輸入需要目前值,以及要求下一個值的處理函式。若元件狀態屬於某項目,應以穩定的項目 ID 作為 key。Key 識別同層元件實例,並不是自動傳入子元件的普通 prop。若選取狀態由父元件持有,篩選可隱藏被選項目而不改變保留的選取值;若從樹中移除持有狀態的子元件,則會卸載該實例。
Effect 在提交後與外部設施同步。建立計時器需要相應的清理。使用適當的空相依陣列時,一般更新會保留正在執行的 interval,卸載時則取消它。不要在渲染期間建立計時器。預測確切生命週期追蹤時,須列明 Strict Mode 及等待更新與 Effect 完成的假設。
React 為每個已掛載的元件身分保留狀態。它再次呼叫元件來渲染時,函式會取得一個新的本地綁定,持有該次渲染的值。較早呼叫傳回的處理函式,仍參照較早呼叫的綁定。這與上面的閉包計數器不同:後者直接以 += 改變保留的 let 綁定,而狀態 setter 不會重新賦值給正在執行的處理函式內的 const count。Setter 要求的是另一個狀態值及另一次渲染。這說明兩個例子雖然都使用閉包,更新追蹤卻不同。
Effect 把已提交的元件連接到其渲染狀態之外的東西,例如 interval 或登記集合。渲染計算畫面;建立操作在畫面提交後設立外部關係;需要時,清理便移除該關係。以相同穩定 key 重新排列項目,會保留其狀態身分;移除某項目則會卸載該身分,並呼叫相應清理。這樣,元件不再存在時,其外部登記也會終止,而不是每次渲染都繼續累積。
用三欄追蹤狀態:該次渲染捕捉的值、React 處理排隊操作時的待處理值,以及已提交的畫面。假設捕捉的 count 為3。兩次以 count + 1 取代的要求,都攜帶數值4,因此處理後仍是4。若改成兩個各自加一的 updater 函式,則依次處理 3 → 4 → 5。處理函式執行時,捕捉的變數仍為3;第二個 updater 接收的卻是待處理值4。畫面在更新提交後才改變。當下一個值依賴前一個排隊值時,應使用 updater;分析篩選、排序或卸載前,則先找出持有狀態的元件。
準備度自檢: 你能追蹤一次排隊更新、在篩選前後辨認狀態持有者,並交代指定步驟中每次計時器建立及清理嗎?
作答方法
- 建議用時:90分鐘。總分:100分。
- 先不執行程式,預測各段輸出。當中間值、物件關係或事件階段有助解釋結果時,應把它們寫出。
- 每題均列明初始條件。各例子互相獨立,一題的變化不會帶入另一題。
- 完成後,利用詳解分辨預測錯誤與論證不足。若獨立推理符合評分條件,便應給予相應的解釋分。
| 題目 | 重點 | 分數 |
|---|---|---|
| Q01 | URL、資源請求與狀態 | 7 |
| Q02 | 語義化 HTML 與字面文字 | 8 |
| Q03 | CSS、盒寬與響應式佈局 | 10 |
| Q04 | 值與獨立閉包 | 10 |
| Q05 | 陣列身分與迭代 | 8 |
| Q06 | DOM 集合與事件傳播 | 10 |
| Q07 | 物件接收者與 JSON | 6 |
| Q08 | 表單項目與驗證 | 9 |
| Q09 | 回應狀態與失敗階段 | 8 |
| Q10 | 非同步執行次序 | 6 |
| Q11 | React 狀態佇列與渲染 | 10 |
| Q12 | React 身分與生命週期 | 8 |
| 總分 | 100 |
Q01 — 解析資源並解釋請求(7 分)
把以下程式視為使用標準 URL API 的獨立 JavaScript 程式。它建立 URL,並不請求相應資源。
const base = new URL("https://notes.example.test:8443/course/week3/index.html");
const target = new URL("../assets/timetable.svg?size=2#caption", base);
console.log(target.href);
console.log(target.origin);
console.log(target.pathname + target.search);
(a) 準確寫出三行輸出。(3 分)
(b) 解釋 ../ 如何改變目錄,以及為何第三行含查詢部分而沒有片段。(2 分)
(c) 在另一個 HTML 網頁中,讀者只改變文件的片段,隨後某處理函式修改段落的 textContent。解釋為何單憑其中任何一個觀察,都不能確立伺服器已儲存新的應用程式資料。(2 分)
Q02 — 閱讀文件的意義(8 分)
考慮以下完整 HTML 文件解析後的狀態,並假設顯示的結構沒有改變。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Session board</title>
</head>
<body>
<main>
<h1>Session board</h1>
<a href="#schedule">Jump to the schedule</a>
<table id="schedule">
<caption>Study sessions</caption>
<thead><tr><th scope="col">Topic</th><th scope="col">Minutes</th></tr></thead>
<tbody><tr><th scope="row">CSS</th><td>25</td></tr></tbody>
</table>
<p>Write <code><em>focus</em></code> to show the markup.</p>
</main>
</body>
</html>
(a) 說明 title、h1 與 main 在這份文件中的不同角色。(3 分)
(b) 利用列與欄的標頭解釋數值 25 的意義,並說明表格標題命名甚麼,以及連結片段識別甚麼。(3 分)
(c) 寫出 code 元素所含的文字。解析這段文字後,該元素內會建立一個 em 後代元素嗎?解釋原因。(2 分)
Q03 — 推導樣式與響應式盒寬(10 分)
以下樣式表套用於 <main id="board"><article class="tile notice">Session</article></main>。所有宣告均為一般、未分層的作者規則,沒有其他相關宣告。在媒體查詢的門檻以下,article 使用初始的 content-box 計算方式;內距與邊框大小均如程式所示。
.tile {
width: 240px;
padding: 12px;
border: 3px solid black;
color: green;
}
#board .tile { color: blue; }
.tile.notice { color: red; }
@media (min-width: 768px) {
.tile { box-sizing: border-box; }
}
(a) Article 套用哪個顏色?明確比較各個競爭的選擇器。(3 分)
(b) 視口寬度為600個 CSS 像素時,計算 article 的內容寬度與邊框盒寬度,並解釋宣告寬度包含哪些部分。(3 分)
(c) 視口寬度為800個 CSS 像素時,再次計算這兩個寬度。(2 分)
(d) 另一個使用 Bootstrap 5.3 預設樣式表的佈局中,一個 .row 內有兩個同層元素,各自的類別為 class="col-12 col-md-6"。沒有其他網格寬度類別。說明它們在600與800個 CSS 像素寬度時的排列。(2 分)
Q04 — 追蹤型別轉換與獨立閉包(10 分)
把以下內容作為一個獨立 JavaScript 程式求值。
console.log(JSON.stringify(["5" + 1, "5" - 1, 0 === "0"]));
function makeCounter(start) {
let value = start;
return delta => {
value += delta;
return value;
};
}
const first = makeCounter(2);
const second = makeCounter(2);
console.log(first(3));
console.log(second(1));
console.log(first(-1));
(a) 準確寫出四行輸出。(4 分)
(b) 分別追蹤 first 與 second 所保留的 value。解釋為何透過其中一個傳回函式進行呼叫,不會更新另一個所保留的值。(4 分)
(c) 解釋為何第一個陣列同時含有字串與數值,以及為何最後一個值為 false。(2 分)
Q05 — 分辨外層陣列與其元素(8 分)
把以下內容作為一個獨立 JavaScript 程式求值。
const sessions = [{ minutes: 20 }, { minutes: 35 }];
const alias = sessions;
const copy = sessions.slice();
copy[0].minutes = 25;
copy.push({ minutes: 10 });
const kept = sessions
.filter(item => item.minutes >= 25)
.map(item => item.minutes / 5);
console.log(JSON.stringify([
sessions.length, alias.length, copy.length, sessions[0].minutes,
]));
console.log(JSON.stringify(kept));
function* slots() {
yield "start";
yield "finish";
return "done";
}
const cursor = slots();
console.log(JSON.stringify([cursor.next(), cursor.next(), cursor.next()]));
(a) 準確寫出三行輸出。(3 分)
(b) 解釋為何 copy[0].minutes = 25 會影響 sessions[0],而附加另一個物件卻只改變 copy 的長度。亦須解釋 filter/map 流程如何得到結果。(3 分)
(c) 解釋為何最後一個產生器結果的 done 為 true。若再呼叫一次 cursor.next(),會傳回甚麼?(2 分)
Q06 — 追蹤節點與冒泡事件(10 分)
以一般 DOM 點擊行為開啟這份完整獨立文件。顯示的元素解析完成後,指令碼執行一次,並剛好呼叫一次最後的 .click()。沒有其他事件監聽器或 DOM 變化。
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Event trace</title></head>
<body>
<ul id="agenda"><li class="slot">A</li><li class="slot">B</li></ul>
<div id="panel">
<button id="choose" type="button"><span id="word">Select</span></button>
</div>
<script>
const snapshot = document.querySelectorAll(".slot");
const live = document.getElementsByClassName("slot");
document.querySelector(".slot").textContent = "Changed";
const item = document.createElement("li");
item.className = "slot";
item.textContent = "C";
document.querySelector("#agenda").appendChild(item);
console.log(`${snapshot.length},${live.length},${snapshot[0].textContent}`);
const panel = document.querySelector("#panel");
const button = document.querySelector("#choose");
button.addEventListener("click", event => {
console.log(`button:${event.target.id}:${event.currentTarget.id}`);
});
panel.addEventListener("click", event => {
console.log(`bubble:${event.target.id}:${event.currentTarget.id}`);
});
document.querySelector("#word").click();
</script>
</body>
</html>
(a) 準確寫出全部三行輸出。(3 分)
(b) 解釋兩個集合的長度及改變後的文字,再說明為何 choose 上的監聽器先於 panel 上的監聽器執行。(4 分)
(c) 在兩行事件輸出中,分別指出哪個值描述事件起始元素、哪個值描述監聽器所在元素。為何兩者不必是同一個元素?(3 分)
Q07 — 分辨身分、資料與方法接收者(6 分)
把以下內容作為一個獨立 JavaScript 程式求值。
const original = {
name: "Sketch",
meta: { count: 1 },
describe() { return this.name; },
};
const alias = original;
const roundTrip = JSON.parse(JSON.stringify(original));
alias.meta.count = 2;
console.log(JSON.stringify([
original === alias,
original === roundTrip,
roundTrip.meta.count,
typeof roundTrip.describe,
]));
console.log(original.describe());
(a) 準確寫出兩行輸出,並解釋身分比較及 roundTrip.meta 內保留的值。(3 分)
(b) 解釋為何 roundTrip.describe 具有輸出所示的型別,以及為何呼叫 original.describe() 會傳回顯示的文字。(3 分)
Q08 — 檢查表單項目與持續的有效性狀態(9 分)
開啟以下獨立文件。指令碼執行一次,使用者沒有提交表單。特別留意,呼叫 new FormData(form) 時沒有傳入提交者引數。這裏的 stepMismatch 回報控制項是否違反步進規則,customError 則回報是否有非空的自訂有效性訊息。
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Form trace</title></head>
<body>
<form id="plan">
<label for="title">Session title</label>
<input id="title" name="title" required value="CSS">
<label for="pages">Pages</label>
<input id="pages" name="pages" type="number" min="1" step="1" required value="3">
<label><input name="include" type="checkbox" value="yes" checked>Include notes</label>
<label><input name="skip" type="checkbox" value="yes">Skip notes</label>
<input name="hint" value="draft" disabled>
<select name="format"><option value="notes" selected>Notes</option></select>
<button name="action" value="save">Save</button>
</form>
<script>
const form = document.querySelector("#plan");
const pages = document.querySelector("#pages");
console.log(JSON.stringify([...new FormData(form)]));
pages.value = "1.5";
console.log(JSON.stringify([pages.validity.stepMismatch, form.checkValidity()]));
pages.setCustomValidity("Whole pages only");
pages.value = "4";
console.log(JSON.stringify([pages.validity.customError, form.checkValidity()]));
pages.setCustomValidity("");
console.log(JSON.stringify([pages.validity.customError, form.checkValidity()]));
</script>
</body>
</html>
(a) 準確寫出第一行輸出,保留項目次序及 pages 的字串值。解釋為何 skip、hint 與 action 沒有出現。(3 分)
(b) 準確寫出最後三行,並解釋步進不符、賦值 "4" 後的自訂錯誤,以及最後的有效性變化。(4 分)
(c) 若 hint 改為啟用、有名稱的唯讀文字控制項,而不是停用控制項,它會提供項目嗎?這種介面狀態會令其值對接收服務而言可信嗎?解釋兩個答案。(2 分)
Q09 — 找出失敗的非同步階段(8 分)
使用標準 Response 與 Promise API 執行以下程式,再等候非同步函式完成。所有回應都在記憶體中建立。最後的 rejected promise 模擬請求在取得可用回應前失敗。
async function inspectResponses() {
const response = await Promise.resolve(new Response('{"message":"later"}', {
status: 503,
headers: { "Content-Type": "application/json" },
}));
console.log(`fulfilled:${response.status}:${response.ok}`);
console.log((await response.json()).message);
try {
await new Response("{broken", { status: 200 }).json();
} catch {
console.log("json-error");
}
try {
await Promise.reject(new TypeError("connection unavailable"));
} catch {
console.log("request-error");
}
}
inspectResponses();
(a) 準確寫出四行輸出。(4 分)
(b) 依次解釋:為何能取得503回應、為何仍能讀取其中的 JSON、哪個操作會因 "{broken" 而失敗,以及最後的拒絕代表哪個較早階段。(4 分)
Q10 — 分開當前任務與其續行(6 分)
把以下完整指令碼視為當前任務,並讓其 promise 續行完成。沒有其他已排程工作。
console.log("A");
Promise.resolve().then(() => console.log("B"));
async function study() {
console.log("C");
await Promise.resolve();
console.log("D");
}
study();
console.log("E");
(a) 依次寫出全部五行輸出。(3 分)
(b) 解釋為何 C 先於 E、為何 E 先於兩個 promise 續行,以及為何 B 先於 D。(3 分)
Q11 — 處理佇列,而非只數 setter 呼叫次數(10 分)
使用目前的 React 元件模型掛載一個全新的 Counter,不使用 StrictMode。等候初始提交完成,記錄按鈕文字。點擊一次,等候該次更新提交後再記錄文字。再點擊一次,等候該次更新提交後再記錄文字。沒有其他更新。
import React, { useState } from "react";
export function Counter() {
const [count, setCount] = useState(1);
return (
<button type="button" onClick={() => {
setCount(count + 1);
setCount(value => value * 3);
setCount(count + 1);
}}>
{count}
</button>
);
}
(a) 把三次記錄的按鈕文字各寫成一行。(3 分)
(b) 就第一次點擊列出佇列的中間值。分辨處理函式所捕捉的 count 與傳入 updater 函式的值,並解釋為何三次 setter 呼叫不會產生三次連續加一。(4 分)
(c) 把第一及第三次呼叫改寫成 updater 函式,保留先加一、再乘三、再加一的次序。改正後,若從全新的 count 值1點擊一次,會顯示甚麼?(3 分)
Q12 — 保留項目身分並平衡生命週期操作(8 分)
不使用 StrictMode,以 items = [{ id: "a", title: "Plan" }, { id: "b", title: "Review" }] 掛載一個全新的 SessionList。等候初始提交與 Effect 完成。匯出的集合起初為空,且只有這個列表使用它。按照以下次序操作,每一步都先等候更新及其 Effect 完成才繼續:
- 依 DOM 次序記錄初始卡片,以
initial:為前綴,其後接上以逗號分隔的id=value配對;每對由卡片 ID 及按鈕的aria-pressed值組成。 - 點擊
a的按鈕,再以marked:為前綴記錄卡片。 - 在同一個 root,以同一批項目物件重新渲染,只把次序改為
[b, a],再以reordered:為前綴記錄卡片。 - 排序
activeSessions中的 ID,並以active:為前綴記錄。 - 卸載 root,等候清理完成,再以
after-unmount:為前綴記錄集合大小。
此集合協助觀察 Effect 的建立與清理,不控制畫面上的釘選狀態。
import React, { useEffect, useState } from "react";
export const activeSessions = new Set();
function SessionCard({ item }) {
const [pinned, setPinned] = useState(false);
useEffect(() => {
activeSessions.add(item.id);
return () => activeSessions.delete(item.id);
}, [item.id]);
return (
<li data-id={item.id}>
<button type="button" aria-pressed={pinned}
onClick={() => setPinned(value => !value)}>
{item.title}
</button>
</li>
);
}
export function SessionList({ items }) {
return <ul>{items.map(item => <SessionCard key={item.id} item={item} />)}</ul>;
}
(a) 準確寫出五行觀察結果。(3 分)
(b) 解釋哪個元件持有 pinned,以及為何以這些 key 重新排序仍會把釘選保留在 a。若使用索引 key,保留的會是哪種關係?(3 分)
(c) 解釋為何重新排序後集合含兩個 ID,而卸載後不含任何 ID。(2 分)
診斷作答結果
使用各小題的評分指引,不要只看總分。若答案錯誤,記下你的模型在哪一步首次與詳解不同。重溫相應複習部分,再先預測一個稍作改動的輸入,然後才重新執行例子。
- 網址或請求判斷錯誤:重溫1.1,分清位址解析、通訊及狀態壽命。
- 元素、跳脫文字或表格關係錯誤:重溫2.1,畫出建立後的文件並標示其關係。
- 顏色、寬度或斷點錯誤:重溫3.1,先列出匹配規則及畫出盒模型,再計算。
- 基本值、閉包或淺層複本錯誤:重溫4.1,記錄中間型別與參照關係。
- 事件或物件結果錯誤:重溫4.2,追蹤實際監聽器與物件,不要只看畫面上的標籤。
- 表單或 promise 結果錯誤:重溫5.1,分清項目、有效性、回應狀態、主體解析及續行次序。
- React 結果錯誤:重溫6.1,寫出待處理的狀態操作,並分清項目身分與位置。