js堆和栈
在 JavaScript 中,堆(Heap) 和 栈(Stack) 是内存管理的两个核心区域,用于存储不同类型的数据,其工作机制直接影响代码的执行效率和变量的生命周期。下面详细解析两者的区别、用途及底层逻辑:
一、栈(Stack):线性存储,自动管理
栈是一种先进后出(LIFO) 的线性数据结构,用于存储基础数据类型和引用类型的地址,其内存分配和释放由 JavaScript 引擎自动完成(无需手动干预)
栈的特点:
-
存储内容:
- 基础数据类型(
Number、String、Boolean、Null、Undefined、Symbol、BigInt)。 - 函数调用时的执行上下文(包括变量、函数参数、作用域链等)。
- 引用类型(如对象、数组)在栈中仅存储内存地址(指向堆中的实际数据)。
- 基础数据类型(
-
内存管理:
- 内存大小固定,分配速度快(编译时即可确定大小)。
- 函数执行结束后,其执行上下文自动从栈中弹出(释放内存),不存在内存泄漏问题。
-
示例:
let a = 10; // 基础类型,直接存储在栈中
let b = a; // 复制栈中的值(10),a和b相互独立
b = 20; // 仅修改b的值,a仍为10
二、堆(Heap):动态存储,手动管理
堆是一块非连续的内存区域,用于存储引用数据类型(对象、数组、函数等),其内存分配和释放由 JavaScript 引擎通过垃圾回收机制间接管理(需手动避免内存泄漏)。
堆的特点:
-
存储内容:
- 引用数据类型的实际值(如
{ name: "foo" }、[1, 2, 3]、函数体等)。 - 内存大小不固定,可动态分配(运行时确定)。
- 引用数据类型的实际值(如
-
内存管理:
- 分配速度较慢(需动态寻找可用内存块)。
- 释放由垃圾回收机制(如标记 - 清除算法)处理:当引用类型失去所有引用时,其占用的堆内存会被自动回收。
-
示例:
let obj1 = { name: "foo" }; // 对象实际存储在堆中,栈中存储堆地址 let obj2 = obj1; // 复制栈中的地址,obj1和obj2指向堆中同一个对象 obj2.name = "bar"; // 修改堆中的数据,obj1.name也会变为"bar"
三、堆和栈的核心区别
| 对比维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 数据类型 | 基础数据类型 + 引用类型的地址 | 引用数据类型(对象、数组、函数等) |
| 内存大小 | 固定(编译时确定) | 动态(运行时分配) |
| 分配 / 释放方式 | 自动(由引擎管理,随执行上下文创建 / 销毁) | 手动逻辑(实际由垃圾回收机制自动释放) |
| 访问速度 | 快(连续内存,直接访问) | 慢(非连续内存,需通过地址查找) |
| 生命周期 | 短(随函数调用结束而释放) | 长(直到无引用被垃圾回收) |
| 数据共享 | 复制值(相互独立) | 共享地址(修改会影响所有引用) |
四、堆和栈的协作流程
当执行一段 JavaScript 代码时,堆和栈的交互过程如下:
-
函数调用时,引擎创建执行上下文并压入栈中,其中包含:
- 变量声明(基础类型直接存栈中,引用类型存地址)。
- 函数参数、作用域链等。
-
引用类型(如对象)的实际值存储在堆中,栈中仅保留指向堆的地址。
-
函数执行结束后,执行上下文从栈中弹出,基础类型内存被释放。
-
堆中不再被引用的数据,会被垃圾回收机制定期清理。
五、常见问题与注意事项
-
为什么基础类型是值传递,引用类型是引用传递?
- 基础类型在栈中直接存储值,赋值时复制的是值本身。
- 引用类型在栈中存储地址,赋值时复制的是地址,因此所有引用指向堆中同一个对象。
-
栈溢出(Stack Overflow)的原因?
- 栈的内存大小固定,若函数递归调用层级过深(如无限递归),会导致执行上下文堆积,超出栈的容量限制,触发栈溢出错误。
-
堆内存泄漏的常见场景?
- 意外的全局变量(不会被回收)。
- 定时器 / 事件监听器未清除,导致回调函数及引用对象一直被保留。
- 闭包中意外持有大对象的引用,导致无法被回收。
理解堆和栈的工作机制,有助于更高效地编写代码(如减少不必要的对象创建、避免内存泄漏),并深入理解 JavaScript 的变量存储和传递逻辑。
js事件循环
在 JavaScript 中,事件循环(Event Loop) 是浏览器或 Node.js 环境中用于处理异步操作的核心机制。由于 JavaScript 是单线程(同一时间只能执行一段代码),事件循环的存在使得它能够非阻塞地处理异步任务(如网络请求、定时器、DOM 事件等),避免了单线程下的性能瓶颈。
一、为什么需要事件循环?
- JavaScript 最初设计为浏览器脚本语言,单线程可以避免多线程操作 DOM 时的冲突(如同时修改和删除同一个元素)。
- 但单线程如果遇到耗时操作(如网络请求、读取文件),会导致程序卡住(阻塞)。事件循环通过异步处理解决了这一问题:让耗时操作在后台执行,主线程继续处理其他任务,待异步操作完成后再回头处理其结果。
二、事件循环的核心概念
要理解事件循环,需先明确以下几个关键组件:
1. 调用栈(Call Stack)
- 用于执行同步代码的地方,遵循先进后出(LIFO) 原则。
- 每执行完一段同步代码,调用栈就会弹出该任务。如果遇到异步任务(如
setTimeout、fetch),会将其交给对应的 ** Web API (浏览器环境)或 Libuv **(Node.js 环境)处理,不会阻塞调用栈。
2. Web API / 后台线程
- 浏览器提供的异步处理模块(如定时器、DOM 事件监听、HTTP 请求等),由独立的后台线程处理(不占用主线程)。
- 当异步任务完成后(如定时器时间到、请求返回数据),会将其回调函数放入任务队列。
3. 任务队列(Task Queue / Callback Queue)
-
存储等待执行的异步任务回调函数,遵循先进先出(FIFO) 原则。
-
任务队列分为两种:
- 宏任务(Macro Task) :包括
setTimeout、setInterval、DOM 事件、fetch网络请求、script标签代码等。 - 微任务(Micro Task) :包括
Promise.then/catch/finally、async/await(本质是 Promise 语法糖)、queueMicrotask等。 - 优先级:微任务的执行优先级高于宏任务,即同一轮事件循环中,所有微任务执行完后才会执行宏任务。
- 宏任务(Macro Task) :包括
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"); // 同步代码
执行步骤解析:
-
执行同步代码
console.log("1")→ 输出1。 -
遇到
setTimeout(宏任务),交给 Web API 处理,计时结束后将回调放入宏任务队列。 -
遇到
Promise.then(微任务),将回调放入微任务队列。 -
执行同步代码
console.log("5")→ 输出5。 -
调用栈为空,事件循环启动:
- 先执行所有微任务:第一个
then输出3,第二个then输出4(微任务队列清空)。 - 微任务执行完,检查宏任务队列,取出
setTimeout回调放入调用栈,执行输出2。
- 先执行所有微任务:第一个
最终输出顺序:1 → 5 → 3 → 4 → 2
五、宏任务与微任务的常见类型
| 类型 | 包含的 API |
|---|---|
| 宏任务 | setTimeout、setInterval、setImmediate(Node.js)、DOM 事件(如 click)、fetch 网络请求、script 整体代码 |
| 微任务 | Promise.then/catch/finally、async/await、queueMicrotask、process.nextTick(Node.js,优先级高于其他微任务) |
六、Node.js 与浏览器事件循环的差异
虽然核心逻辑一致,但 Node.js 的事件循环更复杂,分为 6 个阶段(如 timers、poll、check 等),且微任务中 process.nextTick 优先级最高。而浏览器的事件循环相对简单,主要关注宏任务和微任务的执行顺序。
七、总结
事件循环是 JavaScript 处理异步任务的核心机制,其核心规则可简化为:
- 同步代码优先执行(调用栈非空时不处理任务队列)。
- 调用栈为空时,先执行所有微任务,再执行一个宏任务。
- 重复上述过程,形成循环
注:事件循环是和浏览器的进程线程不可分的,浏览器是多进程多线程的应用程序,为了防止全部崩溃或减少崩溃的几率,分为浏览器进程,网络进程,渲染进程
1.浏览器进程负责界面展示,用户交互(前进后退等),子进程的管理等浏览器进程内部会发起多个线程执行不同的任务
2.网络进程负责加载网络资源,网络进程内部会启动多个线程处理不同的网络任务
3.渲染进程启动后,会开启一个渲染主线程,注线程负责执行HTML,CSS,JS代码,默认情况下浏览器会给每个标签页开启一个渲染主线程,确保不同的标签页之前不相互影响
事件循环发生在渲染主线程中,渲染进程中需要处理的任务是最多的
- 解析html
- 解析css
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60秒
- 执行全局js代码
- 执行事件处理函数
- 执行计时器的回掉函数
如何调度任务
理解事件循环有助于解决异步代码的执行顺序问题(如回调地狱、async/await 的执行逻辑),是进阶 JavaScript 开发的必备知识。
js宏任务与微任务
JavaScript 的事件循环(Event Loop)机制中,宏任务(MacroTask)和微任务(MicroTask)是处理异步任务的两个核心概念,它们决定了异步代码的执行顺序。理解两者的区别对于编写高性能、无 Bug 的异步代码至关重要。
1. 基本概念
宏任务(MacroTask)
-
定义:由浏览器或 Node.js 提供的异步任务,通常是宏观的、独立的操作。
-
常见类型:
setTimeout、setIntervalsetImmediate(Node.js)requestAnimationFrame(浏览器)- I/O 操作(如文件读取、网络请求)
- UI 渲染(浏览器)
微任务(MicroTask)
-
定义:由 JavaScript 引擎自身提供的异步任务,通常是更细粒度的操作,在当前任务执行结束后立即执行。
-
常见类型:
Promise.then/catch/finallyasync/await(本质是 Promise 的语法糖)MutationObserver(浏览器)process.nextTick(Node.js,优先级高于其他微任务)
2. 执行机制:事件循环(Event Loop)
关键步骤
-
执行主线程代码:同步代码按顺序执行。
-
处理微任务队列:
- 主线程代码执行完毕后,立即清空微任务队列(所有微任务按添加顺序执行)。
- 如果执行微任务时又添加了新的微任务,新微任务会被加入队列并继续执行,直到队列为空。
-
处理宏任务队列:
- 微任务队列清空后,从宏任务队列中取出一个任务执行。
- 该宏任务执行完毕后,再次检查并清空微任务队列(重复步骤 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. 同步代码开始和6. 同步代码结束。 -
微任务队列:
- 执行
Promise.then(微任务 2),打印2. Promise微任务执行。 - 微任务 2 中又添加了微任务 2.1,立即执行,打印
3. 嵌套微任务执行。
- 执行
-
宏任务队列:
- 执行
setTimeout回调(宏任务 1),打印4. setTimeout回调执行。 - 宏任务 1 中添加了微任务 1.1,立即执行,打印
5. 宏任务1中的微任务执行。
- 执行
4. 浏览器与 Node.js 的差异
浏览器环境
- 微任务队列在每个宏任务执行后都会被清空。
- 常见场景:
Promise与setTimeout的交互、DOM 更新与渲染时机。
Node.js 环境
-
宏任务分为多个阶段(如
timers、I/O callbacks、check等)。 -
process.nextTick会在每个阶段结束后立即执行,优先级高于其他微任务(如Promise.then)。 -
setImmediate在check阶段执行,与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中终结循环有什么方法
总结:选择合适的终止方法
| 需求 | 方法 | 示例代码 |
|---|---|---|
| 终止当前循环 | break | for (...) { if (...) break; } |
| 跳过当前迭代 | continue | for (...) { if (...) continue; } |
| 终止外层嵌套循环 | 标签 + break | outer: for (...) { break outer; } |
| 终止函数内所有循环 | return | function() { for (...) { return; } } |
| 终止异步数据流循环 | AbortController | controller.abort(); |
| 终止数组方法迭代 | some()/every() | arr.some(item => item > 10); |
| 终止 Generator 函数 | generator.return() | g.return(); |
选择时需注意:
- 优先使用
break/continue处理简单循环。 - 嵌套循环使用标签提升可读性。
- 函数内循环考虑
return。 - 异步场景使用
AbortController或Promise控制。 - 避免在
forEach/map中强行终止(改用for或some)。
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. 核心区别
| 特性 | interface | type |
|---|---|---|
| 定义范围 | 仅用于对象、类、函数等结构 | 支持所有类型(基本类型、联合类型、元组等) |
| 同名合并 | 自动合并同名接口(声明合并) | 重复定义会报错 |
| 扩展方式 | 通过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. 选择建议
-
优先使用 interface:
- 定义对象结构时,interface 语法更简洁,且支持声明合并(利于扩展第三方库)。
- 类实现接口时,代码意图更清晰。
-
使用 type 的场景:
- 需要定义基本类型别名、联合类型、交叉类型、元组等。
- 需要使用映射类型或计算属性(如
keyof)。 - 避免类型碎片化(尽量统一使用 interface 或 type)。
总结
| 场景 | 推荐使用 |
|---|---|
| 对象结构定义 | interface |
| 基本类型 / 联合类型 | type |
| 需声明合并 | interface |
| 需映射类型 / 计算属性 | type |
大多数情况下,interface 和 type 可以互换使用,但理解它们的差异有助于写出更优雅、更符合 TypeScript 设计理念的代码。
any 和 unknown 的区别?如何选择使用any还是unknown?
- any:绕过所有类型检查。
- unknown:必须先做类型检查后才能操作。
在 TypeScript 中,
any和unknown是两种特殊类型,用于处理不确定的类型值。但它们的安全性和使用场景存在本质区别:
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. 关键特性对比表
| 特性 | any | unknown |
|---|---|---|
| 可赋值给任意类型 | ✅ | ❌(需类型断言或类型守卫) |
| 接收任意类型的值 | ✅ | ✅ |
| 访问任意属性 / 方法 | ✅(无类型检查) | ❌(必须先确认类型) |
| 安全性 | 不安全(关闭类型检查) | 安全(强制类型检查) |
| 推荐使用场景 | 临时绕过类型检查(如遗留代码) | 处理未知来源的数据(如用户输入、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. 安全使用建议
-
优先使用
unknown而非any:
当类型不确定时,unknown能强制进行类型检查,避免潜在风险。 -
使用类型守卫缩小
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 } } -
谨慎使用类型断言:
类型断言会绕过类型检查,仅在确定类型时使用:typescript
const value: unknown = "hello"; const strLength: number = (value as string).length; // 确定value是string时使用
总结
| 场景 | 推荐类型 |
|---|---|
| 处理已知类型的动态数据 | 泛型 |
| 处理未知来源的安全数据 | unknown |
| 临时绕过类型检查(遗留代码) | any |
unknown是 TypeScript 中处理不确定类型的首选,它在保持灵活性的同时强制类型安全,而any应作为最后的手段,仅在必要时使用。
webpack工作流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler 对象
- 加载所有配置的插件
- 执行对象的 run 方法开始执行编译
- 根据配置中的entry找出入口文件
- 从入口文件出发,调用所有配置的Loader对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
总结就是三个阶段:
- 初始化:启动构建,读取与合并配置参数,加载
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之后的过程
-
URL解析:浏览器首先会解析输入的URL。URL通常由协议(如HTTP、HTTPS)、域名(或IP地址)、端口号(如果未指定,默认为协议的默认端口)、路径(指定服务器上的资源位置)、查询参数和片段标识符组成。浏览器会将这些部分分解并提取出来,以便后续的操作。
-
DNS解析:如果输入的URL中包含了域名而非IP地址,浏览器会进行DNS解析,将域名解析成相应的IP地址。DNS解析通过向域名服务器发送查询请求,并接收服务器返回的IP地址来完成。一旦浏览器获取了目标服务器的IP地址,它就可以通过该地址与服务器建立连接。
-
建立TCP连接:浏览器使用HTTP协议或HTTPS协议与服务器通信。如果是HTTP协议,浏览器会尝试与服务器的默认HTTP端口(通常是80)建立TCP连接;如果是HTTPS协议,浏览器会尝试与服务器的默认HTTPS端口(通常是443)建立加密的TLS连接。这个过程通常涉及“三次握手”,即浏览器向服务器发送一个连接请求,服务器确认请求并回复,最后浏览器再次确认服务器的回复。
-
发送HTTP请求:一旦TCP连接建立完成,浏览器会向服务器发送HTTP请求。这个请求包含了之前解析得到的URL、请求方法(GET、POST等)、请求头部(包含浏览器和客户端的信息、所需的数据格式等)以及请求体(对于POST请求,通常包含用户提交的数据)。
-
服务器处理请求并返回响应:服务器收到浏览器发送的请求后,会根据请求的内容进行相应的处理。这可能涉及到从服务器上获取请求的资源(如HTML文件、图片、视频等),执行数据库查询、处理用户提交的数据等操作。处理完成后,服务器会生成一个HTTP响应,包含了响应状态码(指示请求的成功或失败)、响应头部(包含服务器信息、内容类型、缓存控制等)以及响应体(所请求资源的实际内容)。
-
接收并渲染响应:浏览器接收到服务器返回的HTTP响应后,会根据响应的内容进行相应的处理。如果响应的内容是HTML,浏览器会解析HTML并构建DOM树,然后根据CSS样式信息构建渲染树,最终将DOM树和渲染树结合起来,展示给用户。如果响应的内容是其他资源(如图片、视频、Java文件等),浏览器会根据其内容类型进行相应的处理,并将其展示在页面上或执行相应的操作。
-
断开连接:一旦浏览器完成了对响应的处理,它会关闭与服务器的TCP连接。在HTTP/1.1中,连接通常会保持一段时间以便于后续的请求,这被称为“持久连接”。在HTTP/2及更新的版本中,多个请求可以通过同一个连接并行处理,以提高性能。