浏览器前进后退为什么可以用两个栈实现?
简化版
浏览器历史可以用两个栈:backStack 存可后退页面,forwardStack 存可前进页面。访问新页面时,当前页压入后退栈,并清空前进栈;后退时当前页进入前进栈,后退栈栈顶变成当前页。
详细版
两个栈正好表达“最近访问的历史状态优先恢复”。
- 新访问页面:当前页进入
backStack,新页面成为 current,forwardStack清空。 - 后退:current 压入
forwardStack,弹出backStack栈顶作为 current。 - 前进:current 压入
backStack,弹出forwardStack栈顶作为 current。 - 不能后退或前进时,对应栈为空。
这题考的不是浏览器内核,而是栈如何表达可撤销状态。重点是新访问页面会清空前进历史,因为原来的未来路径已经失效。
完整版教学
一、为什么历史记录天然有栈特征
后退按钮恢复的是“离当前最近的上一个页面”。如果访问顺序是 A -> B -> C,当前在 C,后退应该回到 B,再后退回到 A。这正是后进先出。
用一个后退栈保存当前页之前的页面:
backStack: [A, B] current: C
栈顶 B 是最近的过去。点击后退时弹出 B,B 成为当前页。这个模型非常直接,因为用户的后退动作就是沿着最近历史一步步倒回去。
二、为什么还需要前进栈
如果从 C 后退到 B,C 并没有消失。用户此时可以点前进回到 C。因此需要一个结构保存“刚刚后退出来的页面”。
这个结构也有栈特征。假设 A -> B -> C -> D,从 D 连续后退两次到 B,那么前进顺序应该先到 C,再到 D。
current: B
forwardStack 栈顶: C,然后是 D
最近被后退出来的 C 应该最先前进回去,所以前进历史也是后进先出。
三、三个操作如何维护状态
可以把状态分成三块:过去、现在、未来。
backStack | current | forwardStack
访问新页面 X:
backStack.push(current);
current = X;
forwardStack.clear();
后退:
forwardStack.push(current);
current = backStack.pop();
前进:
backStack.push(current);
current = forwardStack.pop();
这三段代码表达了历史状态的流动方向。只要这个方向不反,逻辑就不会乱。
四、新访问页面为什么要清空前进栈
这是最容易漏的细节。假设访问 A -> B -> C,后退到 B,此时可以前进到 C。但如果你在 B 访问了新页面 D,历史变成 A -> B -> D,原来的 C 不再是当前路径的未来。
原路径:A -> B -> C
后退到 B 后新访问 D:A -> B -> D
这就像 Git 分支:从旧提交上开了新分支,原来的“前进路径”不再属于当前线性历史。浏览器会清空 forward 历史,避免用户前进到不再连续的旧路径。
五、复杂度和容量限制
每次访问、后退、前进都只涉及栈顶操作,时间复杂度是 O(1)。空间复杂度和保存的历史数量有关,是 O(n)。
实际浏览器不会无限保存历史,可能有容量限制,也可能因为页面进程、缓存策略、安全策略而只保存 URL 或快照的一部分。
| 操作 | backStack | current | forwardStack |
|---|---|---|---|
| 访问新页 | push 旧 current | 新页 | 清空 |
| 后退 | pop | 旧栈顶 | push 旧 current |
| 前进 | push 旧 current | 旧前进栈顶 | pop |
六、这个模型还能迁移到哪些场景
两个栈不仅能做浏览器历史,还能做编辑器撤销/重做、页面导航、状态机回放等。凡是有“回到过去”和“回到未来”的线性状态,都可以考虑这个模型。
记忆钩子:后退栈装过去,前进栈装未来;新访问会改写未来,所以必须清空前进栈。
七、常见误区与追问
- 误区:一个栈就够实现前进后退。 一个栈只能保存过去,无法保存后退后可恢复的未来状态。
- 误区:访问新页面不需要清空 forwardStack。 新页面产生了新路径,旧前进历史已经失效。
- 误区:后退时直接丢弃当前页。 当前页应该进入前进栈,否则无法前进回来。
- 追问:连续后退两次后前进顺序为什么正确? 因为前进栈也是 LIFO,最近退出来的页面最先恢复。
- 追问:空间复杂度是多少? 和历史条目数成正比,通常 O(n),工程实现会设置上限或缓存策略。
八、加强记忆
把浏览器历史想成三段:过去栈、当前页、未来栈。后退让当前页进入未来,过去栈顶变当前;前进让当前页回到过去,未来栈顶变当前;新访问则把当前页压入过去并清空未来。两个栈就是这个状态流动的最小模型。