Skip to content

1.盒模型和bfc

2.浏览器进程

多进程,主要有浏览器主进程、网络进程、gpu进程、工具进程、渲染进程

![](./image/Pasted image 20260525185039.png)

JS 在浏览器主线程中是单线程执行的,但这得益于 Chromium 中其他真实线程(IO、网络、定时器等)的并行支撑,才实现了非阻塞的异步行为。

浏览器真实的线程

线程名主要任务阻塞风险
主线程JS 执行、解析、布局、事件高(导致卡顿)
合成线程滚动、合成动画、分块提交
光栅线程池光栅化(矢量→位图)中(影响首屏速度)
IO 线程IPC 通信、文件 I/O
工作线程池Web Workers、Service Workers独立,不阻塞主线程
从理解 JS 异步和事件循环的角度,通常我们会说渲染进程里有这几个线程(传统浏览器线程模型):
传统浏览器线程模型现代浏览器(如Chrome)线程模型核心职能
GUI渲染线程渲染主线程的一部分负责页面的渲染工作,如解析HTML/CSS,构建DOM树,计算样式和布局(Layout),并生成绘制指令
JS引擎线程渲染主线程负责执行页面中的JavaScript代码。
事件触发线程浏览器底层线程管理事件循环(Event Loop),当用户点击或异步任务完成时,将对应的回调函数推入主线程的任务队列,等待执行
定时器线程浏览器底层线程专门负责setTimeoutsetInterval的计时工作,计时结束后将回调函数放入主线程的任务队列
异步HTTP请求线程浏览器底层线程处理 XMLHttpRequest 或 Fetch 等网络请求,当请求状态变更时,将回调函数放入任务队列
其中最关键的是 JS引擎线程 和 GUI渲染线程 是互斥的,JS执行时会阻塞页面渲染。
  • 事件循环是渲染主线程上运行的一个调度机制,它决定何时执行代码、何时渲染页面。

  • 真实线程(定时器线程、网络进程线程、IO 线程、合成线程等)只负责生产任务接收外部事件,然后通过 IO 线程安全地将任务送入主线程队列。

  • 主线程是唯一执行 JS 和 DOM 渲染的线程(Web Workers 除外),因此事件循环的“循环”发生在主线程上,其他线程只是“投递员”。

3.浏览器从url输入到页面渲染的过程

1. 网络请求阶段

  • URL 输入后,浏览器先进行 DNS 解析(域名→IP)
  • 与服务器建立 TCP 连接(三次握手,HTTPS 还有 TLS 握手)
  • 发送 HTTP 请求,服务器返回 HTML 等资源

2. 解析构建阶段

  • 解析 HTML 生成 DOM 树
  • 解析 CSS 生成 CSSOM 树
  • 遇到 <script> 标签会阻塞 DOM 构建(除非有 async/defer
  • 合并 DOM 和 CSSOM 形成渲染树(Render Tree) 3. 渲染呈现阶段
  • 布局(Layout):计算每个节点的位置和尺寸
  • 绘制(Paint):填充像素、绘制颜色/边框等
  • 合成(Composite):GPU 将图层合并显示到屏幕上

补充一句关键点:CSS 会阻塞渲染(但不阻塞 DOM 解析),JS 会阻塞 DOM 构建,优化时要减少关键路径资源。

极简版本

浏览器先进行 DNS 解析和 TCP 连接,获取 HTML;然后解析 HTML 生成 DOM、解析 CSS 生成 CSSOM,合并成渲染树;最后经过布局、绘制、合成,将页面显示出来。其中 CSS 阻塞渲染,JS 阻塞 DOM 构建。

4.解析html,js脚本阻塞解析该怎么解决

原因

js脚本阻塞是因为在解析html的过程中,如果遇到script标签,它会执行标签内的代码,然后才会继续解析其他内容。 根本原因是js代码可以操作dom树,更改html结构,浏览器为了保证一致性,所以会等待脚本执行完成后才继续后续的解析和渲染。

解决

(1)将 <script> 放在 <body> 底部

javascript
<body>
  <!-- 所有HTML内容 -->
  <script src="app.js"></script>
</body>

(2)script增加async或defer属性

异步脚本:async是在脚本下载完成后立即执行,执行过程中会阻塞html渲染。执行顺序取决于下载完成的时间,不保证与声明顺序一致。适合广告类独立无依赖的脚本,不需要保证执行顺序。 延迟脚本:defer是在html解析完成后执行,是按照声明顺序执行。适合需要操作完整dom的场景使用。 他们两个都是在下载时不阻塞渲染。渲染时机上有差别。

(3)使用es模块化代码

type="module"的script代码默认为延迟代码

(4)动态注入脚本

在合适的时机动态创建和注入脚本,使用appendChild

现在可以使用按需加载(import()),另外可以使用web worker不占用主线程(纯计算任务)

5.计算机网络7层模型,实际工程中为什么使用4层模型

  • OSI 七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
  • TCP/IP 四层:网络接口层(有的书拆成物理+数据链路)、网络层、传输层、应用层。 在实际开发中,会话层表示层的职责被证明可以非常灵活地合并到应用层。
  • 会话层(负责建立/管理会话):Web 开发中,登录状态、Session 管理、断点续传,都是直接在应用层代码里实现的(比如 HTTP 头加 Cookie),没必要单独抽出一层。
  • 表示层(负责加密、压缩、转码):如今,数据加密是 SSL/TLS 在传输层做,图片压缩是应用层代码(如 WebP 格式)自己处理。再加一层反而增加调用开销。

“作为前端开发,我们日常接触的 HTTP/HTTPS 就在应用层。虽然 OSI 有七层,但我们实际调试时:

  • 写代码时只关心应用层(接口数据、Header)。
  • 抓包时看到传输层(TCP 握手、端口)。
  • 偶尔遇到 IP 问题才看网络层。
  • 物理层和数据链路层(如网卡故障、路由器冲突)通常由运维或基础设施团队处理。 会话和表示层的概念在 HTTP 协议演进中其实被吸收了,比如 HTTP/2 的多路复用 就隐式实现了会话层功能,但并没必要因此增加一个独立的层级。”

因为 OSI 提出的会话层和表示层在实际工程中被证明可以放入应用层灵活处理,加上 TCP/IP 先入为主占据了整个互联网,所以全世界最终用了四层模型。

6.HTTP/2 的多路复用

HTTP/2 的多路复用,核心是在一个 TCP 连接上,把多个请求和响应拆分成独立的帧,并交错发送,接收端再根据流 ID 重组。这样就解决了 HTTP/1.1 中请求必须排队等待的队头阻塞问题,可以同时并发上百个请求,而不用开多个连接。以前为了性能做的那些域名分片、精灵图、内联资源,在 HTTP/2 下都变得没必要了。当然,它仍然受 TCP 丢包的影响。为了保证数据的有序性,一个流的包丢失,其他流必须暂停等待。http3每个流是独立的,一个流的包丢失不会影响其他流。

  • HTTP/2:TCP连接是基于IP地址和端口号的,一旦IP变了,连接就中断了。必须重新进行一次完整的握手来建立新连接,这个过程中视频会卡顿,正在上传的文件也会中断

  • HTTP/3:QUIC引入了一个独特的“连接ID”(Connection ID,即连接标识符)。无论你的IP地址如何变化,只要这个ID不变,QUIC就能识别出这是同一个连接。这意味着,当你在不同网络间切换时,HTTP/3连接可以保持活跃,不会中断,提供了非常流畅的移动网络体验。

  • HTTP/1.1和HTTP/2的加密是可选的(虽然大多数情况都开启了HTTPS),但HTTP/3强制所有通信都必须经过加密

  • 尽管HTTP/3在弱网环境下优势巨大,但也有一些研究发现,在高速、稳定的有线网络(如千兆光纤)中,HTTP/3的速度有时反而比HTTP/2稍慢。这主要是因为QUIC基于UDP,在用户态实现,带来了更高的CPU开销和更复杂的处理逻辑

7.http底层基于什么协议,有什么缺点,怎么解决

HTTP是基于TCP/IP协议,位于应用层,面向连接且可靠

核心缺陷表现描述演进解决方案
无状态服务器默认不“记住”用户,每次请求都是独立的应用层技术:Cookie与Session、Token (如JWT)
明文传输HTTP/1.x时代数据以纯文本形式传输,存在被窃听或篡改的风险协议升级:HTTPS (HTTP + SSL/TLS 加密)
队头阻塞前一个请求未完成,后一个请求必须等待,导致页面加载变慢协议演进:HTTP/2 (多路复用) 和 HTTP/3 (独立流传输)

(1). 如何解决“无状态”(跨请求记住用户)?

先理解无状态:HTTP协议认为每个请求都是独立的、全新的

为什么HTTP要设计成“无状态”? 这是为了让协议简单、快速、可扩展

  • 服务器不必为每个用户维护一份“记忆”(内存占用、CPU开销),可以专心处理当前请求。
  • 在海量并发请求下,无状态让服务器很容易横向扩展(加机器就行),因为任何一台服务器处理请求都不需要知道用户之前在其他服务器上做了什么。

这是前端面试高频考点,关键在于向面试官展示你对CookieSessionToken的理解。回答时可以优先强调 Cookie + Session 的经典组合,再补充无状态的 Token 方案

  • Cookie 和 Session:服务器创建 Session 存储用户状态,并在浏览器设置一个名为 Session ID 的 Cookie。浏览器后续请求自动携带该 Cookie,服务器通过它找到对应的 Session,从而“记住”用户。该方法的缺点是状态存在服务端内存,不利于水平扩展(Session共享问题),以及Cookie可能被用户禁用

  • Token(如JWT):服务器不存储状态,而是生成一个包含用户信息的加密字符串(Token),客户端需在每次请求的Authorization头中携带。服务器通过验证Token的签名来识别和信任用户。由于服务器不存储状态,它对分布式系统更友好,Token也更适用于移动端APP。

(2). 如何解决“明文传输”(保证通信安全)?

标准答案直接给出 HTTPS 即可。如果面试官继续追问,可以展开讲讲它的核心机制。

  • 解决方案:HTTPS = HTTP + SSL/TLS 加密协议。

  • 核心机制(回答有深度):面试官可能会追问加密原理,你可以解释混合加密

    1. 建立安全通道时,采用 非对称加密 (RSA等) 安全地交换一个临时的“会话密钥”

    2. 后续的大量数据传输,则切换为效率更高的 对称加密 (AES等) 进行加密

    3. 同时,数字证书作为网站的身份证明,防止中间人伪造服务器

(3). 如何解决“队头阻塞”(加快网络请求速度)?

这个问题主要考察你对HTTP/1.x、HTTP/2、HTTP/3特性的理解。可以按照协议演进的时间线来回答:

  • HTTP/1.1时代:浏览器通过域名分片(对同一页面资源使用多个域名),以及精灵图(Sprites)文件合并等构建时优化来规避浏览器并发限制。但这些方法不优雅,且无法彻底解决问题
  • HTTP/2时代:引入 多路复用 (Multiplexing)。在一个TCP连接上,同时交错发送多个请求和响应,解决了应用层队头阻塞。同时,还有头部压缩二进制分帧服务器推送等特性
  • HTTP/3时代:引入 QUIC 协议。它基于UDP,彻底解决了TCP层队头阻塞,即某个流丢包不会阻塞其他流。同时,它还能0-RTT快速建立连接连接迁移

“HTTP的核心缺陷主要有三个:无状态、明文传输和队头阻塞。无状态通过Cookie/Session或Token解决;明文传输由HTTPS协议接管;而队头阻塞则通过HTTP/2的多路复用和HTTP/3的QUIC协议在持续优化。”

8.vue项目越写越大有什么缺点、该如何优化

单文件臃肿,几千行代码。拆分ui组件和逻辑组件,做到组件单一职责。抽取公共函数和组合函数,多复用。 ts项目用any比较多,开启 vue-tsc 的 strict 模式,禁止显式 any。 项目成员多,每个人代码风格不一,配置eslint统一代码风格 部分纯展示不变化的数据,使用shallowRef或者markRaw,tab标签切换和模态框这种可以使用组件懒加载,使用defineAsyncComponent动态引入

9.js的内存模型

JS 内存模型主要分为(存储基本类型值和引用地址)和(存储实际对象)。函数调用通过执行上下文栈管理,闭包会导致外部变量无法释放。垃圾回收采用标记清除为主,循环引用不是问题(但引用计数算法是)。

10.内存泄漏的排查思路

前端开发中,JavaScript 的垃圾回收(GC)会自动管理内存,但一些不合理的代码会意外地保持对象引用,使 GC 无法回收它们 常见内存泄漏包括:意外全局变量、未清定时器、脱离 DOM 的引用(JS 变量仍引用已被移除的 DOM 节点,导致该 DOM 无法被回收。)、滥用闭包。 使用开发者工具调试、快照对比

11.js的垃圾回收机制

JavaScript 具有自动内存管理,开发者不需要手动分配和释放内存。垃圾回收(GC)会定期找出不再使用的变量并释放其占用的内存。 基于“可达性”的标记-清除:

  • 原理
    • 标记阶段:从根(root,如全局对象、调用栈上的变量)出发,递归访问所有对象,能访问到的标记为“可达”
    • 清除阶段:遍历堆内存,回收未标记的对象 采用 可达性(reachability) 算法:从根(如 window、正在执行的函数上下文、DOM 树根等)出发,能遍历到的对象就是“可达”的,不会被回收;遍历不到的才是“不可达”的,会被回收。 可达性思想和标记-清除算法是垃圾回收领域的通用理论基础,V8 只是它的一个实现者(并且在许多方面做了深度优化)。

12.判断二叉搜索树

13.手写防抖和节流函数

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
function throttle(fn, interval) {
  let timer = null;
  return function(...args) {
    if (timer) return;
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, interval);
  };
}

14.apply,call,bind的区别

都是用于显式绑定函数执行时的 this 上下文。它们的核心区别体现在调用时机参数传递方式上。

apply是立即执行,参数是数组(类数组)。参数列表形式:fn.call(thisArg, arg1, arg2, ...) call是立即执行,参数逐个传递,逗号分隔。参数数组形式:fn.apply(thisArg, [argsArray]) bind是返回一个新的函数,不会立即执行。绑定this和预设参数:fn.bind(thisArg, arg1, arg2, ...) 返回值:call/apply是函数执行结果,bind是函数。

  1. 箭头函数:箭头函数的 this 在定义时已经确定(基于外层作用域),无法被 call/apply/bind 改变。
  2. bind 的永久绑定bind 返回的函数即使再次使用 call/apply/bind 也无法改变其 this
  3. new 绑定 > 显式绑定(call/apply/bind) > 隐式绑定 > 默认绑定。任何绑定都无法覆盖 new

15.this的指向问题

核心规则:this 的指向由调用方式决定 this 和函数定义位置完全没关系,只和“调用方式”有关。但是箭头函数中的this它根本不看谁调用,它只看:定义时外层的 this 是谁。

调用方式this 指向示例环境
普通函数调用全局对象(浏览器 window,严格模式 undefinedfn()
对象方法调用调用该方法的对象obj.fn()。但是方法赋值给变量后调用,this 变为全局。
构造函数调用(new新创建的实例对象new Fn()
箭头函数定义时所在的外层作用域的 this(词法绑定)() => {}
call/apply/bind手动指定的第一个参数fn.call(obj)
事件处理函数(DOM)绑定事件的元素(e.currentTargetel.onclick = fn
定时器/回调(非箭头)全局对象(严格模式 undefinedsetTimeout(fn, 0)
  • 谁调用就指向谁(方法调用)
  • 独立调用指全局(非严格)
  • new 最高级,bind 硬绑定
  • 箭头函数无自己的 this,看外层
类型说明
全局作用域最外层
函数作用域函数内部
块级作用域{} 内部

16.图片懒加载和路由懒加载是怎么实现的

图片懒加载

(1).图片元素可以增加自定义属性data-src,原来的src可以只用占位图片,懒加载替换真是图片。监听滚动事件,然后使用getBoundingClientRect方法获取元素相对于视口的位置,进去范围内然后替换为真实图片。window.innerheight获取视口高度。

javascript
<img data-src="real-image.jpg" src="placeholder.jpg" alt="lazy" />

<script>
  const lazyImages = document.querySelectorAll('img[data-src]');
  
  function loadImage(img) {
    const src = img.dataset.src;
    if (!src) return;
    img.src = src;
    img.removeAttribute('data-src');
  }
  
  function checkAndLoad() {
    lazyImages.forEach(img => {
      const rect = img.getBoundingClientRect();
      // 判断是否进入可视区域(可根据需要增加提前量)
      if (rect.top < window.innerHeight && rect.bottom > 0) {
        loadImage(img);
      }
    });
  }
  
  // 监听滚动事件,注意加节流
  window.addEventListener('scroll', throttle(checkAndLoad, 200));
  window.addEventListener('resize', throttle(checkAndLoad, 200));
  checkAndLoad(); // 初始检查
</script>

(2).可以使用IntersectionObserver可视区域检测,无需监听滚动事件。新建一个实例对象observer,对象传入一个方法,方法内可以判断通过目标元素是否进入是否范围,然后拿到target进行操作替换路径。然后获取到图片元素列表循环调用实例对象上的observe即可。 entries 是一个 IntersectionObserverEntry 对象的数组(是数组是因为一个 IntersectionObserver 实例可以同时观察多个元素。当它们的状态同时发生变化时(比如页面滚动导致多个元素同时进入视口),回调函数会一次性收到所有相关元素的 entry,而不是每个元素触发一次回调,从而提升性能。)。每个 IntersectionObserverEntry 包含目标元素与根元素(或视口)的交叉信息。需要列出关键属性:target(目标元素)、isIntersecting(是否交叉)

javascript
<img data-src="real-image.jpg" class="lazy" />

<script>
  const observer = new IntersectionObserver((entries) => {

    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img); // 加载后停止观察。一旦图片开始加载,就不再需要关心它是否还在可视区域,立即 `unobserve` 可以让观察器只关注尚未开始加载的元素,提升性能。
      }
    });
  }, {
    rootMargin: '0px 0px 100px 0px', // 提前100px触发
    threshold: 0.01
  });
  
  document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));
</script>

(3)可以只用自带的给img标签添加loading="lazy" 属性,浏览器自动处理。

路由懒加载

vue中路由懒加载是使用import方法,然后打包回处理成独立的块chunk

17.pinia和vuex的区别

19.axios的二次封装

20.token如果过期了该怎么处理

21.vue中父子组件通信

22.v-if和v-show的区别

基于 Obsidian + VitePress 构建