浏览器
强缓存
- Expires
- 浏览器请求服务端资源时,请求响应会携带 Expires 头,标识资源过期时间
- 缺点是以来客户端本地时间,其不会判断资源是否变更
- Cache-Control
- max-age:缓存失效相对时间,单位为秒
- s-maxage:中间的缓存服务器上的缓存失效时间
- no-cache:不使用强缓存,但是会使用协商缓存
- no-store:不使用任何缓存
- public:可以被所有使用者缓存(包涵中间的缓存服务器)
- private:只能被发起请求的用户自己缓存
- only-if-cached:用户只接受中间缓存服务器的资源
协商缓存
- 当强缓存失效后,浏览器请求携带协商缓存请求头,由服务端判断是否需要进缓存
- Last-Modified:服务器返回给浏览器的资源最近修改时间
- If-Modified-Since:浏览器请求服务端携带的资源最近修改时间
- Etag:资源标识,服务端返回给浏览器
- If-None-Match:浏览器才能服务端获取的资源标识 Etag 的值
- 浏览器请求服务端资源时会携带 If-Modified-Since 和 If-None-Match 两个请求头,服务端根据这两个头判断资源是否失效,若未失效则返回给浏览器304,让浏览器从缓存中读取文件
渲染流程
- 寻址并获取文档
- 浏览器从DNS缓存文件中查找域名对应的IP
- 从操作系统的缓存文件(hosts)中查找
- 从本地域名服务器进行查找
- 迭代查询
- 本地域名服务器会先向根域名服务器发送请求获取顶级域名服务器地址
- 本地域名服务器向顶级域名服务器发送DNS解析请求,顶级域名服务器返回权威域名服务器地址
- 本地域名服务器向权威域名服务器地址发送DNS解析请求,获取到域名的IP地址
- 递归查询
- 本地域名服务器向根域名服务器发送查询请求
- 根域名服务器向顶级域名服务器发送查询请求
- 顶级域名服务器向权威域名服务器发送查询请求
- 若某一个环节查到,则会进行缓存且按照查询的路径返回
- 本地服务器查询到域名的IP地址后,就返回浏览器进行缓存并开始建立TCP链接,三次握手和HTTPS过程见计算机网络模块
- 文档解析
- 构建DOM树
- 从请求的HTML文档中读取到文档字符串
- 将文档字符串转换为Token,每一个标签代码都会被转成一个Token对象
- Token对象的生成是通过元素标签的开始标签和结束标签判断的,根据子节点标签是否在父节点开始和结束标签之间来判断父子关系
- 当一个Token对象产生后,会立即通过该Token对象创建DOM节点对象,同时会继续生成新的Token对象
- 当所有的DOM节点创建完成后即形成了DOM树
- 构建CSSOM树
- CSSOM树的构建过程与DOM树类似,都是从字符串转成Token对象再创建对应的节点对象
- CSS解析时通过递归去获得每个元素的全部样式
- 其他过程
- 构建DOM树过程中如果遇到script标签,会去加载JS代码,如果未使用async和defer,则会等到JS代码解析执行完成才能继续
- 浏览器也会同时去构建一个适用于无障碍模式的Accessibility的DOM树
- 构建DOM树
- 页面渲染
- 构建渲染树
- 从DOM树根节点开始遍历其每个子节点,找到每个节点对应的CSS样式规则并应用
- 不可见的节点在遍历中会被跳过
- 布局
- 当渲染树构建完成后,从根节点开始遍历,计算每个节点的宽高、位置等信息
- 第一次确定节点的宽高和位置信息称为布局,后面的每一次变化都称为回流
- 回流由于会重新计算节点位置信息,开销较大,且回流一定会触发重绘
- 绘制
- 绘制过程将前面布局计算好的节点绘制在浏览器窗口
- 当元素的任何属性发生变化时,都会进行“重绘”操作来更新元素
- 构建渲染树
内核
- 浏览器内核包含渲染引擎和JS引擎两部分
- 渲染引擎负责读取页面内容(XML、图片、CSS等)并处理渲染逻辑,最终展示在视窗内
- JS引擎负责解析和执行JS代码,处理JS中的事件, 实现页面的动态交互
浏览器不同标签页怎么通信
- 找中介者,如后端服务,使用websocket协议保持长连接
- 使用postMessage
- 使用localStorage或sessionStorage,通过监听localStorage进行通信
- 使用shareWorker
- 允许同源的多个浏览器上下文(如多个标签页、iframe)共享同一个后台线程,实现高效的跨页面通信和资源共享。
- 在shareWorker中,可以维护一个共享的状态或数据,所有连接到该shareWorker的页面都可以访问和修改这个状态。
js
// shared-worker.js
onconnect = (event) => {
const port = event.ports[0]; // 获取连接端口
// 监听来自页面的消息
port.onmessage = (e) => {
console.log('Received from page:', e.data);
// 处理消息并回复
port.postMessage(`Worker processed: ${e.data}`);
};
// 可选:主动向页面发送消息
// port.postMessage('Worker initialized');
};js
// 主页面代码
const worker = new SharedWorker('shared-worker.js', 'my-shared-worker');
// 获取通信端口
const port = worker.port;
// 监听 Worker 消息
port.onmessage = (e) => {
console.log('Received from worker:', e.data);
};
// 向 Worker 发送消息
port.postMessage('Hello from main page');
// 启动端口连接(Chrome 需要,Firefox 自动启动)
port.start();- 使用broadCast Channel API
js
// 登录成功后广播
const channel = new BroadcastChannel('auth-status');
channel.postMessage({ type: 'login', user: 'john' });
// 其他标签页监听
channel.onmessage = (event) => {
if (event.data.type === 'login') {
updateUI({ user: event.data.user });
}
};常见存储类型
cookie,localStorage,sessionStorage,indexedDB
浏览器怎么解析成dom树的
- 通过词法分析和语法分析实现的
- 词法分析:是将源码分割为独立含义的token单元,核心任务是扫码识别单词
- 语法分析:处理一串词法单元,验证语法正确性。
不会冒泡的事件
UI事件(load,unload),焦点事件(blur、focus),鼠标事件(mouseleave、mouseenter)
demo
iframe页面架构,怎么在主页面返回的时候实际上返回的是iframe页面
通过结合 postMessage 和 History API,可以实现点击浏览器返回按钮时让 iframe 导航,而非主页面导航。核心在于拦截主页面的返回事件,同步 iframe 的历史记录,并伪造主页面的历史记录点。
- history.pushState() 和 history.replaceState() 是 HTML5 新增的 API,用于手动修改浏览器历史记录,其核心特点是:
- 不触发页面刷新:调用后浏览器地址栏会更新,但页面不会重新加载。
- 仅修改历史栈:相当于在历史记录中 “插入” 或 “替换” 一条记录,不影响当前页面的 DOM 和状态。
- 触发 popstate 事件:当用户点击浏览器前进 / 后退按钮时,才会触发 popstate 事件,此时可拦截并执行自定义逻辑(如控制 iframe 导航)。 主页面逻辑:
html
<!-- 主页面 -->
<body>
<!-- 主页面自身内容(不会刷新) -->
<h1>主页面内容(保持不动)</h1>
<iframe id="myIframe" src="https://iframe-domain.com/page1"></iframe>
<script>
const iframe = document.getElementById('myIframe');
// 存储 iframe 的历史记录(主页面维护,避免 iframe 自身历史栈干扰)
let iframeHistory = ['https://iframe-domain.com/page1']; // 初始页面
// 初始化:向 iframe 同步初始历史记录
iframe.onload = () => {
postToIframe({ type: 'init', history: iframeHistory });
};
// 向 iframe 发送消息(控制其导航)
function postToIframe(data) {
iframe.contentWindow.postMessage(data, 'https://iframe-domain.com');
}
// 监听 iframe 发送的消息(如 iframe 内部导航时通知主页面)
window.addEventListener('message', (event) => {
if (event.origin !== 'https://iframe-domain.com') return;
const { type, url } = event.data;
if (type === 'navigate') {
// iframe 内部发生导航(如用户点击 iframe 内的链接)
// 1. 更新主页面维护的 iframe 历史记录
iframeHistory.push(url);
// 2. 用 pushState 向主页面历史栈添加一条“虚拟记录”
// (地址栏会更新,但主页面不刷新)
history.pushState(
{ type: 'iframe-nav', url: url }, // 存储额外信息(供 popstate 时使用)
'', // 标题(多数浏览器忽略)
window.location.href // 地址栏保持不变(或自定义,如添加 hash)
);
}
});
// 拦截浏览器返回按钮事件
window.addEventListener('popstate', (event) => {
// 若 iframe 历史记录只有1条(初始页),则允许主页面正常后退
if (iframeHistory.length <= 1) return;
// 1. 移除 iframe 历史记录的最后一条(当前页)
iframeHistory.pop();
// 2. 获取 iframe 要返回的上一页 URL
const targetUrl = iframeHistory[iframeHistory.length - 1];
// 3. 通知 iframe 导航到上一页(仅 iframe 刷新,主页面不动)
postToIframe({ type: 'back', url: targetUrl });
// 4. 用 replaceState 修正主页面历史记录(避免地址栏异常)
history.replaceState(
{ type: 'iframe-nav', url: targetUrl },
'',
window.location.href // 地址栏保持不变
);
});
</script>
</body>iframe 页面逻辑:
html
<!-- iframe 页面 -->
<body>
<h2>iframe 页面 1</h2>
<a href="https://iframe-domain.com/page2" id="link">跳转到页面2</a>
<script>
// 存储主页面同步的历史记录
let iframeHistory = [];
// 监听主页面的消息
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent-domain.com') return;
const { type, url, history } = event.data;
if (type === 'init') {
// 初始化历史记录
iframeHistory = history;
} else if (type === 'back') {
// 主页面通知 iframe 后退,直接导航到目标 URL
window.location.href = url;
}
});
// 监听 iframe 内部的导航(如点击链接)
document.getElementById('link').addEventListener('click', (e) => {
e.preventDefault(); // 阻止默认跳转(由主页面控制历史记录)
const newUrl = e.target.href;
// 1. 手动跳转到新页面(iframe 内部刷新)
window.location.href = newUrl;
// 2. 通知主页面更新历史记录
window.parent.postMessage(
{ type: 'navigate', url: newUrl },
'https://parent-domain.com'
);
});
</script>
</body>