CSCI2720 · 阶段模拟 A
复习路线
本练习覆盖从浏览器/服务器基础到 React 组件的七个教学单元。先沿着下面的路线复习,再独立作答,完成后才打开详解与评分指引。建议用时90分钟、总分100分,是这套阶段练习自行采用的学习安排。
1.1 浏览器、服务器与网页资源
从资源及负责处理它的程序开始分析。HTTP请求标明操作与资源;响应提供状态、首部和资源表示。一个可见页面可以由分别请求取得的HTML文档、样式表、脚本及图片组成。统计请求数量前,应先说明缓存、重定向和额外依赖资源的假设;请求数量本身并不能确定资源一定按严格的先后顺序加载。
应能区分URL的协议、主机、端口、路径、查询和片段。根据文档的基准URL解析相对URL,而不是直接拼接字符串。同源判断中的源由协议、主机及端口组成。片段可以选择文档中的位置,但不属于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及点击处理函数后,能否推导最终资源URL,并区分请求目标与片段?能否准确说明哪些值会保留到下一次点击,哪些值会保留到新文档实例?
2.1 语义化HTML文档
把源代码看成对关系的描述。元素承担某种角色,标签界定其源代码,属性则提供目标地址、标识符或类名等值。应区分文档标题与可见标题、导航与普通文字集合,以及数据表格与纯粹的视觉排列。一个类名本身既不提供样式,也不实现行为。
阅读表格时,找出表题、行标题、列标题,以及各数据单元格与这些标题的关系。阅读图片时,区分有意义的替代文本与装饰图片所用的空替代文本。要显示字面的源代码,应正确编码<和&,而不是预期浏览器原样显示标记。字符引用会在DOM中变成字符;不会因为解码得到一串类似标签的文字,就再解析一次并建立新元素。
假设某个表格单元格写着40,行标题是DOM,列标题是Minutes。结合两种关系来读,它记录的是一次40分钟的DOM学习时段。如果没有列标题,40也可能表示页数或分数。表格为这个值提供含义,却不会把单元格的DOM文本字符串"40"自动变成应用数值。如果代码要把两个学习时段的时长相加,就应读取文本并明确完成所需转换。选择页面标题与表格标题时,应让这些关系在视觉布局改变后仍然成立,而不是只因为某个标签的默认字体合适就使用它。
准备自检: 能否根据含义选择元素,通过行列标题解释某个表格单元格,并区分字面文本<em>focus</em>与真正的em元素?
3.1 响应式CSS与Bootstrap
首先判断哪些声明适用于目标元素。在同一作者来源、普通声明且未分层的条件下,先比较选择器优先级,再比较源代码顺序。直接作用于元素的声明与继承来的值不同。仅仅把规则从外部样式表移到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修改类名标记。分析方法调用时应找出接收调用的对象,不应假定把函数移到别处后仍保留相同的this。JSON是表示适当数据的文本;解析会构造具有新对象身份的值,也不会恢复方法。
设想有三个匹配的列表节点,同时记录一个静态选择结果和一个实时集合。把第一个节点的标签从Draft改成Ready,再追加第四个匹配节点。静态结果仍包含三个节点引用,但它的第一个节点现在显示Ready;实时集合则包含四个节点。静态结果固定的是成员,不是各节点旧属性的一张照片。如果操作应该作用于最初选中的节点,就选固定结果;如果意图是观察当前的匹配成员,就选实时集合。在遍历实时集合的同时修改树时,要把成员在迭代过程中改变的情况算进去,不能假定这些索引仍然描述最初那组节点。
准备自检: 能否追踪嵌套元素上的点击,给出静态与实时集合当前的长度,并区分对象别名与独立解析出来的JSON对象?
5.1 表单、验证与Fetch
构造提交条目前,先列出符合提交条件的具名控件。禁用控件会被省略;只读的具名控件可以贡献其值。未勾选的复选框不产生条目。可见标签和id不能代替实际用于提交的name。
区分控件的原始字符串值、应用所需的值,以及明确转换后得到的数值。required并不能表达所有应用规则:空格可能满足文本控件的非空要求,却不满足“必须含可见字符”的规则。自定义有效性信息在程序清除前会一直生效。界面约束不能代替接收端的验证。
分阶段追踪异步工作:取得响应、检查状态、读取响应体、解释所得数据。HTTP错误响应仍可能使Fetch操作兑现;状态成功后,读取无效JSON仍可能被拒绝。await之前的工作在当前调用中就会开始,之后的续执行则稍后发生。某次读取若依赖先前写入,就应在那次写入完成后进行。
把值的轨迹与有效性的轨迹分开。数字输入可以持有原始字符串"6";检查相关输入约束后,明确进行数值转换会得到6,因此再加两页得到8,而不是拼接文本"62"。现在假设程序设置了一条自定义错误信息。把控件改成另一个符合数字规则的值,会改变输入值,但那条自定义信息仍是独立的有效性状态,直到被清除才消失。请求之后也应再次区分层次:取得响应后才能读取状态,状态用来区分HTTP成功与HTTP错误,而读取响应体得到的数据还要按照应用约定解释。即使错误响应,也可能携带有用的诊断数据。不能仅因为取得了响应,就清除学习者的输入。
准备自检: 能否分别预测表单条目和有效性,识别请求轨迹中失败的具体阶段,并解释内存服务的响应对数据存续时间到底作出了什么保证?
6.1 组件、状态与渲染
把组件描述为当前props和state所决定的视图。渲染计算界面,事件处理函数请求状态更新,后续提交才改变DOM。运行中的处理函数捕获的状态变量,仍是那一次渲染的快照。根据同一快照反复请求替换值,与接收前一个待处理结果的更新函数,含义不同。
受控输入需要同时提供当前值,以及请求下一个值的处理函数。当组件状态属于特定条目时,应使用稳定的条目ID作为key。key标识同级组件实例,并不是自动传给子组件的普通prop。如果选择状态属于父组件,筛选可以把选中的条目隐藏,但不改变保留的选择;若把拥有状态的子组件从树中移除,则会卸载那个实例。
Effect在提交后与外部设施同步。建立计时器时应配对清理操作。使用合适的空依赖数组时,普通更新会保留正在运行的定时器,而卸载会将其取消。不要在渲染过程中创建计时器。预测精确的生命周期轨迹时,应明确Strict Mode以及提交、Effect完成的条件。
React为每个已挂载组件身份保留状态。当它再次调用组件进行渲染时,函数会为这次渲染的值建立一个新的局部绑定。较早那次调用返回的处理函数,仍引用较早那次调用的绑定。这与前面的闭包计数器不同:普通计数器通过+=直接修改保留的let绑定,而状态设置函数不会重新给正在运行的处理函数中的const count赋值。它请求另一个状态值以及另一次渲染。这个区别解释了为什么两个例子都使用闭包,更新轨迹却不同。
Effect把已提交的组件与其渲染状态之外的事物连接起来,例如定时器或登记集合。渲染计算视图;视图提交后,建立操作才创建外部关系,清理操作则在需要时移除该关系。使用相同稳定key重新排列条目,会保留它们的状态身份;移除条目则会卸载其身份,并调用相应的清理。这使外部登记能够在组件不再存在时停止,而不是每次渲染后不断累积。
把状态轨迹分成三列:该次渲染捕获的值、React处理排队操作时的待处理值,以及已提交的显示。假设捕获的count为3,两次请求都用count + 1替换状态,那么携带的都是数值4,处理完后仍为4。若改为两个各加一的更新函数,则依次处理为3 → 4 → 5。处理函数运行时,捕获的变量仍为3,但第二个更新函数收到的是待处理值4。显示在更新提交时改变。如果下一个值依赖前一个排队结果,应采用更新函数;在分析筛选、重排或卸载之前,则应先找出拥有状态的组件。
准备自检: 能否追踪一次排队更新,在筛选过程中识别状态的拥有者,并逐项核对指定操作序列中每次计时器建立与清理?
使用方法
- 建议用时: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 分)
把下面代码作为独立JavaScript程序运行,使用标准URL API。程序只构造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>。所有声明都是未分层的普通作者规则,没有其他相关声明。在媒体查询阈值以下,文章使用初始的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) 文章采用哪种颜色?明确比较互相竞争的选择器。(3分)
(b) 视口宽度为600个CSS像素时,计算文章的内容宽度与边框盒宽度,并解释声明宽度包含哪些部分。(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的长度。解释筛选/映射流水线如何得到其结果。(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运行代码,并等待异步函数完成。所有响应均在内存中构造。最后一个拒绝的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与传给更新函数的值。说明为什么调用三次setter并不等于连续递增三次。(4分)
(c) 把第一次和第三次调用改写为更新函数,保持“加一、乘三、再加一”的操作顺序。修复后,从全新的初始值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:记录卡片。 - 用相同的条目对象,按
[b, a]顺序重新渲染同一个根,然后用前缀reordered:记录卡片。 - 将
activeSessions中的ID排序,用前缀active:记录。 - 卸载根,等待清理完成,然后用前缀
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,而卸载后一个也没有。(2分)
分析作答结果
使用各小问的评分指引,不要只看总分。对于错误答案,找出自己的推理模型第一次偏离详解的步骤。重温对应复习部分后,先预测一个略作改变的输入,再重新运行示例。
- URL或请求判断错误:重温1.1,区分地址解析、通信与状态的存续时间。
- 元素、转义文本或表格关系错误:重温2.1,画出构造后的文档并标注关系。
- 颜色、宽度或断点错误:重温3.1,计算前先列出匹配规则并画出盒子。
- 基本值、闭包或浅复制错误:重温4.1,记录中间类型与引用关系。
- 事件或对象结果错误:重温4.2,追踪实际监听器与对象,而不只看可见标签。
- 表单或Promise结果错误:重温5.1,区分条目、有效性、响应状态、响应体解析与续执行顺序。
- React结果错误:重温6.1,写出待处理的状态操作,并区分条目身份与位置。