JS/TS/webpack面试

162 阅读24分钟

js堆和栈

在 JavaScript 中,堆(Heap) 和 栈(Stack) 是内存管理的两个核心区域,用于存储不同类型的数据,其工作机制直接影响代码的执行效率和变量的生命周期。下面详细解析两者的区别、用途及底层逻辑:

一、栈(Stack):线性存储,自动管理

栈是一种先进后出(LIFO)  的线性数据结构,用于存储基础数据类型引用类型的地址,其内存分配和释放由 JavaScript 引擎自动完成(无需手动干预)

栈的特点:

  1. 存储内容

    • 基础数据类型(NumberStringBooleanNullUndefinedSymbolBigInt)。
    • 函数调用时的执行上下文(包括变量、函数参数、作用域链等)。
    • 引用类型(如对象、数组)在栈中仅存储内存地址(指向堆中的实际数据)。
  2. 内存管理

    • 内存大小固定,分配速度快(编译时即可确定大小)。
    • 函数执行结束后,其执行上下文自动从栈中弹出(释放内存),不存在内存泄漏问题。
  3. 示例


let a = 10; // 基础类型,直接存储在栈中
let b = a;  // 复制栈中的值(10),a和b相互独立
b = 20;     // 仅修改b的值,a仍为10

二、堆(Heap):动态存储,手动管理

堆是一块非连续的内存区域,用于存储引用数据类型(对象、数组、函数等),其内存分配和释放由 JavaScript 引擎通过垃圾回收机制间接管理(需手动避免内存泄漏)。

堆的特点:

  1. 存储内容

    • 引用数据类型的实际值(如 { name: "foo" }[1, 2, 3]、函数体等)。
    • 内存大小不固定,可动态分配(运行时确定)。
  2. 内存管理

    • 分配速度较慢(需动态寻找可用内存块)。
    • 释放由垃圾回收机制(如标记 - 清除算法)处理:当引用类型失去所有引用时,其占用的堆内存会被自动回收。
  3. 示例

    let obj1 = { name: "foo" }; // 对象实际存储在堆中,栈中存储堆地址
    let obj2 = obj1;            // 复制栈中的地址,obj1和obj2指向堆中同一个对象
    obj2.name = "bar";          // 修改堆中的数据,obj1.name也会变为"bar"
    

三、堆和栈的核心区别

对比维度栈(Stack)堆(Heap)
数据类型基础数据类型 + 引用类型的地址引用数据类型(对象、数组、函数等)
内存大小固定(编译时确定)动态(运行时分配)
分配 / 释放方式自动(由引擎管理,随执行上下文创建 / 销毁)手动逻辑(实际由垃圾回收机制自动释放)
访问速度快(连续内存,直接访问)慢(非连续内存,需通过地址查找)
生命周期短(随函数调用结束而释放)长(直到无引用被垃圾回收)
数据共享复制值(相互独立)共享地址(修改会影响所有引用)

四、堆和栈的协作流程

当执行一段 JavaScript 代码时,堆和栈的交互过程如下:

  1. 函数调用时,引擎创建执行上下文并压入栈中,其中包含:

    • 变量声明(基础类型直接存栈中,引用类型存地址)。
    • 函数参数、作用域链等。
  2. 引用类型(如对象)的实际值存储在堆中,栈中仅保留指向堆的地址。

  3. 函数执行结束后,执行上下文从栈中弹出,基础类型内存被释放。

  4. 堆中不再被引用的数据,会被垃圾回收机制定期清理。

五、常见问题与注意事项

  1. 为什么基础类型是值传递,引用类型是引用传递?

    • 基础类型在栈中直接存储值,赋值时复制的是值本身。
    • 引用类型在栈中存储地址,赋值时复制的是地址,因此所有引用指向堆中同一个对象。
  2. 栈溢出(Stack Overflow)的原因?

    • 栈的内存大小固定,若函数递归调用层级过深(如无限递归),会导致执行上下文堆积,超出栈的容量限制,触发栈溢出错误。
  3. 堆内存泄漏的常见场景?

    • 意外的全局变量(不会被回收)。
    • 定时器 / 事件监听器未清除,导致回调函数及引用对象一直被保留。
    • 闭包中意外持有大对象的引用,导致无法被回收。

理解堆和栈的工作机制,有助于更高效地编写代码(如减少不必要的对象创建、避免内存泄漏),并深入理解 JavaScript 的变量存储和传递逻辑。

js事件循环

在 JavaScript 中,事件循环(Event Loop)  是浏览器或 Node.js 环境中用于处理异步操作的核心机制。由于 JavaScript 是单线程(同一时间只能执行一段代码),事件循环的存在使得它能够非阻塞地处理异步任务(如网络请求、定时器、DOM 事件等),避免了单线程下的性能瓶颈。

一、为什么需要事件循环?

  • JavaScript 最初设计为浏览器脚本语言,单线程可以避免多线程操作 DOM 时的冲突(如同时修改和删除同一个元素)。
  • 但单线程如果遇到耗时操作(如网络请求、读取文件),会导致程序卡住(阻塞)。事件循环通过异步处理解决了这一问题:让耗时操作在后台执行,主线程继续处理其他任务,待异步操作完成后再回头处理其结果。

二、事件循环的核心概念

要理解事件循环,需先明确以下几个关键组件:

1. 调用栈(Call Stack)
  • 用于执行同步代码的地方,遵循先进后出(LIFO)  原则。
  • 每执行完一段同步代码,调用栈就会弹出该任务。如果遇到异步任务(如 setTimeoutfetch),会将其交给对应的 ** Web API  (浏览器环境)或 Libuv **(Node.js 环境)处理,不会阻塞调用栈。
2. Web API / 后台线程
  • 浏览器提供的异步处理模块(如定时器、DOM 事件监听、HTTP 请求等),由独立的后台线程处理(不占用主线程)。
  • 当异步任务完成后(如定时器时间到、请求返回数据),会将其回调函数放入任务队列
3. 任务队列(Task Queue / Callback Queue)
  • 存储等待执行的异步任务回调函数,遵循先进先出(FIFO)  原则。

  • 任务队列分为两种:

    • 宏任务(Macro Task) :包括 setTimeoutsetIntervalDOM 事件fetch 网络请求、script 标签代码等。
    • 微任务(Micro Task) :包括 Promise.then/catch/finallyasync/await(本质是 Promise 语法糖)、queueMicrotask 等。
    • 优先级:微任务的执行优先级高于宏任务,即同一轮事件循环中,所有微任务执行完后才会执行宏任务。
4. 事件循环(Event Loop)
  • 持续监控调用栈任务队列的运行机制:

    • 当调用栈为空(同步代码执行完毕),事件循环会先检查微任务队列,如果有微任务,依次执行所有微任务(直到微任务队列为空)。
    • 微任务执行完后,检查是否需要重新渲染页面(如 DOM 变更)。
    • 最后检查宏任务队列,取出一个宏任务放入调用栈执行,执行完后重复上述流程。

三、事件循环的执行流程(图解)

plaintext

[调用栈] → 执行同步代码 → 遇到异步任务 → 交给 Web API 处理
       ↓
Web API 完成任务 → 将回调放入对应任务队列(微任务/宏任务)
       ↓
调用栈为空 → 事件循环启动 → 先执行所有微任务 → 再执行一个宏任务
       ↓
重复上述流程...

四、示例:直观理解事件循环

以下代码可清晰展示事件循环的执行顺序:

javascript

console.log("1"); // 同步代码

setTimeout(() => {
  console.log("2"); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log("3"); // 微任务
}).then(() => {
  console.log("4"); // 微任务(链式调用仍为微任务)
});

console.log("5"); // 同步代码

执行步骤解析

  1. 执行同步代码 console.log("1") → 输出 1

  2. 遇到 setTimeout(宏任务),交给 Web API 处理,计时结束后将回调放入宏任务队列

  3. 遇到 Promise.then(微任务),将回调放入微任务队列

  4. 执行同步代码 console.log("5") → 输出 5

  5. 调用栈为空,事件循环启动:

    • 先执行所有微任务:第一个 then 输出 3,第二个 then 输出 4(微任务队列清空)。
    • 微任务执行完,检查宏任务队列,取出 setTimeout 回调放入调用栈,执行输出 2

最终输出顺序1 → 5 → 3 → 4 → 2

五、宏任务与微任务的常见类型

类型包含的 API
宏任务setTimeoutsetIntervalsetImmediate(Node.js)、DOM 事件(如 click)、fetch 网络请求、script 整体代码
微任务Promise.then/catch/finallyasync/awaitqueueMicrotaskprocess.nextTick(Node.js,优先级高于其他微任务)

六、Node.js 与浏览器事件循环的差异

虽然核心逻辑一致,但 Node.js 的事件循环更复杂,分为 6 个阶段(如 timerspollcheck 等),且微任务中 process.nextTick 优先级最高。而浏览器的事件循环相对简单,主要关注宏任务和微任务的执行顺序。

七、总结

事件循环是 JavaScript 处理异步任务的核心机制,其核心规则可简化为:

  1. 同步代码优先执行(调用栈非空时不处理任务队列)。
  2. 调用栈为空时,先执行所有微任务,再执行一个宏任务。
  3. 重复上述过程,形成循环

注:事件循环是和浏览器的进程线程不可分的,浏览器是多进程多线程的应用程序,为了防止全部崩溃或减少崩溃的几率,分为浏览器进程,网络进程,渲染进程

1.浏览器进程负责界面展示,用户交互(前进后退等),子进程的管理等浏览器进程内部会发起多个线程执行不同的任务 
2.网络进程负责加载网络资源,网络进程内部会启动多个线程处理不同的网络任务
3.渲染进程启动后,会开启一个渲染主线程,注线程负责执行HTML,CSS,JS代码,默认情况下浏览器会给每个标签页开启一个渲染主线程,确保不同的标签页之前不相互影响

事件循环发生在渲染主线程中,渲染进程中需要处理的任务是最多的

  • 解析html
  • 解析css
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画60秒
  • 执行全局js代码
  • 执行事件处理函数
  • 执行计时器的回掉函数

如何调度任务

理解事件循环有助于解决异步代码的执行顺序问题(如回调地狱、async/await 的执行逻辑),是进阶 JavaScript 开发的必备知识。

js宏任务与微任务

JavaScript 的事件循环(Event Loop)机制中,宏任务(MacroTask)和微任务(MicroTask)是处理异步任务的两个核心概念,它们决定了异步代码的执行顺序。理解两者的区别对于编写高性能、无 Bug 的异步代码至关重要。

1. 基本概念

宏任务(MacroTask)
  • 定义:由浏览器或 Node.js 提供的异步任务,通常是宏观的、独立的操作。

  • 常见类型

    • setTimeoutsetInterval
    • setImmediate(Node.js)
    • requestAnimationFrame(浏览器)
    • I/O 操作(如文件读取、网络请求)
    • UI 渲染(浏览器)
微任务(MicroTask)
  • 定义:由 JavaScript 引擎自身提供的异步任务,通常是更细粒度的操作,在当前任务执行结束后立即执行。

  • 常见类型

    • Promise.then/catch/finally
    • async/await(本质是 Promise 的语法糖)
    • MutationObserver(浏览器)
    • process.nextTick(Node.js,优先级高于其他微任务)

2. 执行机制:事件循环(Event Loop)

关键步骤
  1. 执行主线程代码:同步代码按顺序执行。

  2. 处理微任务队列

    • 主线程代码执行完毕后,立即清空微任务队列(所有微任务按添加顺序执行)。
    • 如果执行微任务时又添加了新的微任务,新微任务会被加入队列并继续执行,直到队列为空。
  3. 处理宏任务队列

    • 微任务队列清空后,从宏任务队列中取出一个任务执行。
    • 该宏任务执行完毕后,再次检查并清空微任务队列(重复步骤 2)。
    • 重复此过程,形成循环。
执行顺序总结

plaintext

执行同步代码 → 清空微任务队列(递归处理新增微任务) → 执行一个宏任务 → 再次清空微任务队列 → 执行下一个宏任务 → ...

3. 经典示例:宏任务与微任务的执行顺序

javascript

console.log('1. 同步代码开始');

// 宏任务1
setTimeout(() => {
  console.log('4. setTimeout回调执行');
  // 微任务1.1(在宏任务1执行时添加)
  Promise.resolve().then(() => {
    console.log('5. 宏任务1中的微任务执行');
  });
}, 0);

// 微任务2
Promise.resolve().then(() => {
  console.log('2. Promise微任务执行');
  // 微任务2.1(在微任务2执行时添加)
  Promise.resolve().then(() => {
    console.log('3. 嵌套微任务执行');
  });
});

console.log('6. 同步代码结束');
执行结果分析

plaintext

1. 同步代码开始
6. 同步代码结束
2. Promise微任务执行
3. 嵌套微任务执行
4. setTimeout回调执行
5. 宏任务1中的微任务执行
执行流程
  1. 同步代码:打印1. 同步代码开始6. 同步代码结束

  2. 微任务队列

    • 执行Promise.then(微任务 2),打印2. Promise微任务执行
    • 微任务 2 中又添加了微任务 2.1,立即执行,打印3. 嵌套微任务执行
  3. 宏任务队列

    • 执行setTimeout回调(宏任务 1),打印4. setTimeout回调执行
    • 宏任务 1 中添加了微任务 1.1,立即执行,打印5. 宏任务1中的微任务执行

4. 浏览器与 Node.js 的差异

浏览器环境
  • 微任务队列在每个宏任务执行后都会被清空。
  • 常见场景:PromisesetTimeout的交互、DOM 更新与渲染时机。
Node.js 环境
  • 宏任务分为多个阶段(如timersI/O callbackscheck等)。

  • process.nextTick会在每个阶段结束后立即执行,优先级高于其他微任务(如Promise.then)。

  • setImmediatecheck阶段执行,与setTimeout执行顺序取决于事件循环的时机。

Node.js 示例

javascript

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

// 执行结果不确定,取决于事件循环的时机
// 可能先打印setTimeout,也可能先打印setImmediate

5. 实际应用场景

(1)DOM 更新优化
  • 使用MutationObserver(微任务)监听 DOM 变化,在渲染前批量处理更新。
  • 避免在setTimeout(宏任务)中频繁操作 DOM,导致多次重排 / 重绘。
(2)异步操作顺序控制
  • Promise.then(微任务)确保某些操作在当前任务结束后立即执行。
  • setTimeout(宏任务)将耗时操作推迟到下一个事件循环,避免阻塞 UI。
(3)错误捕获
  • 微任务中的错误会在当前事件循环结束前抛出,宏任务中的错误会在下一个事件循环抛出。

6. 总结:何时使用宏任务 / 微任务?

场景推荐使用原因
需要立即执行的异步操作微任务(Promise)微任务会在当前任务结束后立即执行,无需等待下一个宏任务周期。
需要延迟执行的操作宏任务(setTimeout)宏任务会在下一个事件循环周期执行,可避免阻塞当前任务。
批量处理 DOM 更新微任务(MutationObserver)微任务在渲染前执行,可批量处理 DOM 变化,减少渲染次数。
模拟 “同步执行” 的异步操作微任务(Promise)微任务的执行时机最接近同步代码,可用于实现类似 “原子操作” 的效果。

理解宏任务和微任务的执行机制,能帮助你更好地控制异步代码的执行顺序,避免因时序问题导致的 Bug,提升代码的稳定性和性能。

js中终结循环有什么方法

总结:选择合适的终止方法

需求方法示例代码
终止当前循环breakfor (...) { if (...) break; }
跳过当前迭代continuefor (...) { if (...) continue; }
终止外层嵌套循环标签 + breakouter: for (...) { break outer; }
终止函数内所有循环returnfunction() { for (...) { return; } }
终止异步数据流循环AbortControllercontroller.abort();
终止数组方法迭代some()/every()arr.some(item => item > 10);
终止 Generator 函数generator.return()g.return();

选择时需注意:

  • 优先使用break/continue处理简单循环。
  • 嵌套循环使用标签提升可读性。
  • 函数内循环考虑return
  • 异步场景使用AbortControllerPromise控制。
  • 避免在forEach/map中强行终止(改用forsome)。

TS篇

ts中type 与 interface 的区别

在 TypeScript 中,type(类型别名)和interface(接口)都用于定义类型,但它们在语法、功能和使用场景上存在一些关键区别。以下是详细对比:

1. 语法差异

interface

使用interface关键字,只能定义对象类型:

typescript

interface User {
  name: string;
  age: number;
  greet: () => void;
}
type

使用type关键字,可定义任意类型(基本类型、联合类型、交叉类型等):

typescript

// 基本类型别名
type Name = string;

// 联合类型
type Status = 'active' | 'inactive';

// 交叉类型
type AdminUser = User & { role: string };

// 元组
type Point = [number, number];

// 函数类型
type Handler = (event: Event) => void;

2. 核心区别

特性interfacetype
定义范围仅用于对象、类、函数等结构支持所有类型(基本类型、联合类型、元组等)
同名合并自动合并同名接口(声明合并)重复定义会报错
扩展方式通过extends关键字通过&交叉类型
实现类类可implements接口类可implements具备对象形式的 type
映射类型不支持直接映射支持(如Readonly<T>
计算属性不支持支持(如keyof

3. 具体差异示例

(1)定义范围
  • interface

    interface User {
      name: string;
      age: number;
    }
    
  • type

    type User = {
      name: string;
      age: number;
    };
    
    type ID = string | number; // interface无法定义
    type Pair<T> = [T, T];     // 泛型元组
    

S

(2)同名合并(声明合并)
  • interface

    typescript

    interface User {
      name: string;
    }
    
    interface User {
      age: number; // 与上面的User合并为{ name: string; age: number }
    }
    
  • type

    type User = {
      name: string;
    };
    
    type User = { // 报错:重复定义
      age: number;
    };
    
(3)扩展方式
  • interface

    interface Animal {
      name: string;
    }
    
    interface Dog extends Animal {
      bark(): void;
    }
    
  • type

    type Animal = {
      name: string;
    };
    
    type Dog = Animal & {
      bark(): void;
    };
    
(4)实现类
  • interface

    interface User {
      name: string;
    }
    
    class Admin implements User {
      name = 'Admin'; // 必须实现name属性
    }
    
  • type

    type User = {
      name: string;
    };
    
    class Admin implements User { // 同样有效
      name = 'Admin';
    }
    

4. 高级特性支持

(1)映射类型
  • type

    type ReadonlyUser = Readonly<User>;
    
(2)计算属性
  • type

    type Keys = keyof User; // 'name' | 'age'
    

5. 选择建议

  1. 优先使用 interface

    • 定义对象结构时,interface 语法更简洁,且支持声明合并(利于扩展第三方库)。
    • 类实现接口时,代码意图更清晰。
  2. 使用 type 的场景

    • 需要定义基本类型别名、联合类型、交叉类型、元组等。
    • 需要使用映射类型或计算属性(如keyof)。
    • 避免类型碎片化(尽量统一使用 interface 或 type)。

总结

场景推荐使用
对象结构定义interface
基本类型 / 联合类型type
需声明合并interface
需映射类型 / 计算属性type

大多数情况下,interface 和 type 可以互换使用,但理解它们的差异有助于写出更优雅、更符合 TypeScript 设计理念的代码。

any 和 unknown 的区别?如何选择使用any还是unknown?

  • any:绕过所有类型检查。
  • unknown:必须先做类型检查后才能操作。 在 TypeScript 中,anyunknown是两种特殊类型,用于处理不确定的类型值。但它们的安全性和使用场景存在本质区别:

1. 核心区别

any
  • 任意类型的 “通行证” :完全绕过类型检查,可赋值给任何类型,也可接收任何类型的值。
  • 类型系统的 “逃逸舱” :使用any会关闭类型检查,可能导致运行时错误。
unknown
  • 安全的 “未知类型” :表示类型不确定,但在使用前必须先进行类型检查或类型断言。
  • 类型系统的 “安全网” :强制用户在使用前确认类型,避免意外的类型错误。

2. 对比示例

赋值兼容性

typescript

let value: unknown;

value = 123;         // 合法,unknown可接收任何类型
value = "hello";     // 合法
value = { name: "Alice" }; // 合法

let num: number;
num = value;         // 报错:unknown不能直接赋值给其他类型

// 需要先进行类型断言或类型守卫
if (typeof value === 'number') {
  num = value;       // 合法,已确认类型为number
}
any的对比

typescript

let anyValue: any;
let unknownValue: unknown;

// 赋值给其他类型
let num1: number = anyValue;    // 合法,any可赋值给任意类型
let num2: number = unknownValue; // 报错,unknown不可直接赋值

// 访问属性和方法
anyValue.foo();      // 合法,不检查属性是否存在
unknownValue.foo();  // 报错,必须先确认类型

3. 关键特性对比表

特性anyunknown
可赋值给任意类型❌(需类型断言或类型守卫)
接收任意类型的值
访问任意属性 / 方法✅(无类型检查)❌(必须先确认类型)
安全性不安全(关闭类型检查)安全(强制类型检查)
推荐使用场景临时绕过类型检查(如遗留代码)处理未知来源的数据(如用户输入、API 响应)

4. 典型使用场景

使用any的场景
  • 处理无法确定类型的遗留代码

    typescript

    // 调用第三方库返回的未知类型
    declare function legacyApi(): any;
    const result = legacyApi(); // 使用any暂时绕过类型检查
    
  • 临时快速实现

    typescript

    // 开发阶段临时使用,后续需完善类型
    const data: any = JSON.parse(localStorage.getItem('data'));
    
使用unknown的场景
  • 处理用户输入或外部 API 数据

    typescript

    async function fetchData() {
      const response = await fetch('api/data');
      const data: unknown = await response.json(); // 使用unknown确保安全
      
      // 使用前进行类型检查
      if (typeof data === 'object' && data !== null && 'name' in data) {
        console.log(data.name); // 此时data被缩小为{ name: unknown }
      }
    }
    
  • 函数返回不确定类型

    typescript

    function parseInput(input: string): unknown {
      try {
        return JSON.parse(input);
      } catch {
        return input;
      }
    }
    

5. 安全使用建议

  1. 优先使用unknown而非any
    当类型不确定时,unknown能强制进行类型检查,避免潜在风险。

  2. 使用类型守卫缩小unknown范围

    typescript

    function processValue(value: unknown) {
      if (Array.isArray(value)) {
        value.forEach(item => console.log(item)); // value被缩小为any[]
      } else if (typeof value === 'number') {
        console.log(value * 2); // value被缩小为number
      }
    }
    
  3. 谨慎使用类型断言
    类型断言会绕过类型检查,仅在确定类型时使用:

    typescript

    const value: unknown = "hello";
    const strLength: number = (value as string).length; // 确定value是string时使用
    

总结

场景推荐类型
处理已知类型的动态数据泛型
处理未知来源的安全数据unknown
临时绕过类型检查(遗留代码)any

unknown是 TypeScript 中处理不确定类型的首选,它在保持灵活性的同时强制类型安全,而any应作为最后的手段,仅在必要时使用。

webpack工作流程

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
  2. 用上一步得到的参数初始化 Compiler 对象
  3. 加载所有配置的插件
  4. 执行对象的 run 方法开始执行编译
  5. 根据配置中的entry找出入口文件
  6. 从入口文件出发,调用所有配置的Loader对模块进行编译
  7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  9. 再把每个 Chunk 转换成一个单独的文件加入到输出列表
  10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

总结就是三个阶段:

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

webpack run过程

执行npm run dev时候最先执行的build/dev-server.js文件,该文件主要完成下面几件事情:

1、检查node和npm的版本、引入相关插件配置

2、webpack对源码进行编译打包并返回compiler对象

3、创建express服务器

4、配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware)

5、挂载代理服务和中间件

6、配置静态资源

7、启动服务器监听特定端口(8080)

8、自动打开浏览器并打开特定网址(localhost:8080)

浏览器输入url之后的过程

  1. URL解析:浏览器首先会解析输入的URL。URL通常由协议(如HTTP、HTTPS)、域名(或IP地址)、端口号(如果未指定,默认为协议的默认端口)、路径(指定服务器上的资源位置)、查询参数和片段标识符组成。浏览器会将这些部分分解并提取出来,以便后续的操作。

  2. DNS解析:如果输入的URL中包含了域名而非IP地址,浏览器会进行DNS解析,将域名解析成相应的IP地址。DNS解析通过向域名服务器发送查询请求,并接收服务器返回的IP地址来完成。一旦浏览器获取了目标服务器的IP地址,它就可以通过该地址与服务器建立连接。

  3. 建立TCP连接:浏览器使用HTTP协议或HTTPS协议与服务器通信。如果是HTTP协议,浏览器会尝试与服务器的默认HTTP端口(通常是80)建立TCP连接;如果是HTTPS协议,浏览器会尝试与服务器的默认HTTPS端口(通常是443)建立加密的TLS连接。这个过程通常涉及“三次握手”,即浏览器向服务器发送一个连接请求,服务器确认请求并回复,最后浏览器再次确认服务器的回复。

  4. 发送HTTP请求:一旦TCP连接建立完成,浏览器会向服务器发送HTTP请求。这个请求包含了之前解析得到的URL、请求方法(GET、POST等)、请求头部(包含浏览器和客户端的信息、所需的数据格式等)以及请求体(对于POST请求,通常包含用户提交的数据)。

  5. 服务器处理请求并返回响应:服务器收到浏览器发送的请求后,会根据请求的内容进行相应的处理。这可能涉及到从服务器上获取请求的资源(如HTML文件、图片、视频等),执行数据库查询、处理用户提交的数据等操作。处理完成后,服务器会生成一个HTTP响应,包含了响应状态码(指示请求的成功或失败)、响应头部(包含服务器信息、内容类型、缓存控制等)以及响应体(所请求资源的实际内容)。

  6. 接收并渲染响应:浏览器接收到服务器返回的HTTP响应后,会根据响应的内容进行相应的处理。如果响应的内容是HTML,浏览器会解析HTML并构建DOM树,然后根据CSS样式信息构建渲染树,最终将DOM树和渲染树结合起来,展示给用户。如果响应的内容是其他资源(如图片、视频、Java文件等),浏览器会根据其内容类型进行相应的处理,并将其展示在页面上或执行相应的操作。

  7. 断开连接:一旦浏览器完成了对响应的处理,它会关闭与服务器的TCP连接。在HTTP/1.1中,连接通常会保持一段时间以便于后续的请求,这被称为“持久连接”。在HTTP/2及更新的版本中,多个请求可以通过同一个连接并行处理,以提高性能。