布莱克的充电站

世界上只有一种真正的英雄主义,就是在认清生活的真相之后依然热爱生活——罗曼·罗兰

展开
输入地址到浏览器展示页面全流程
最近了解了一下CDN,所以想完整的梳理一下从输入地址到页面展示经历的全流程 1.URL 解析 浏览器首先判断你输入的是关键词还是合法 URL。如果是关键词,调用默认搜索引擎;如果是 URL,则补全协议(如自动将 example.com 补全为 [https://example.com] 2.DNS解析 DNS:把 域名 (如 www.google.com)转换成 计算机能识别的 IP 地址 如果浏览器没有缓存该域名的 IP,它会启动 DNS 查找 浏览器缓存 -> 操作系统缓存(Hosts 文件) -> 路由器缓存 -> ISP(互联网服务提供商)DNS 缓存 CDN 的介入 如果你的架构中配置了 CDN,权威 DNS 最终不会直接返回源站 IP,而是返回一个 CNAME 记录。 这个 CNAME 指向 CDN 的 GSLB(全局负载均衡系统)。 GSLB 会根据用户的地理位置、IP 所属运营商、各 CDN 节点的负载情况,返回一个离用户最近、最健康的 CDN 边缘节点 IP 3.建立网络连接 拿到 IP 后,浏览器与服务器(或 CDN 节点)开始建立 TCP 连接 第一次握手: 客户端发送 SYN 报文,进入 SYN_SENT 状态。 第二次握手: 服务端收到后,同意连接并发送 SYN-ACK 报文,进入 SYN_RCVD 状态。 第三次握手: 客户端收到后,发送 ACK 报文,双方进入 ESTABLISHED 状态 如果是 HTTPS,在 TCP 建连后必须进行 TLS 握手(以 TLS 1.3 为例,已从 TLS 1.2 的两次 RTT 优化为 1次 RTT ): 客户端发送支持的密码套件和临时公钥(Client Hello)。 服务端返回选定的加密算法、服务端证书和它的临时公钥(Server Hello)。 双方通过 ECDHE 算法 计算出对称加密的“会话密钥”,后续所有流量均通过此密钥加密。 4.发送HTTP请求数据传输 HTTP/2: 在同一个 TCP 连接上进行 多路复用 ,通过二进制分帧层打破了 HTTP/1.1 的队头阻塞(Head-of-line blocking),允许同时并发无数个请求。 请求到达 CDN 边缘节点: 缓存命中(Hit): CDN 节点直接返回缓存的 index.html。 缓存失效/未命中(Miss): CDN 节点向源站(Origin Server)发起回源请求。源站可能是你的 Nginx,再反向代理到 Node.js 或 Java 应用程序,生成 HTML 后返回给 CDN,CDN 再缓存并转发给用户 5.浏览器解析与渲染 构建 DOM 树(Document Object Model) 渲染进程的 Tokeniser 开始将 HTML 字符串解析成 Token,并根据嵌套关系构建成一颗树状结构的 DOM。 流式解析: 浏览器不需要等整个 HTML 下载完,而是下载多少就解析多少。 预加载扫描器(Preload Scanner): 当主线程在解析 HTML 时,另一个轻量级线程会快速扫描后面的 HTML,发现 script、link 等外链资源后,提前发起异步下载,防止网络阻塞。 2. 构建 CSSOM 树(CSS Object Model) 当遇到 <link rel="stylesheet"> 或 <style> 时,浏览器会并行下载并解析 CSS,生成 CSSOM 树。 CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 的渲染。(因为没有样式,渲染出来的页面会出现闪烁/白屏)。 CSS 会阻塞后续 JS 的执行。(因为 JS 可能会操作 CSSOM,JS 必须等待 CSSOM 构建完成才能运行)。 3. 处理 JavaScript(阻塞的艺术) 当解析器遇到 <script> 标签时: 普通脚本: 暂停 DOM 解析,下载并执行 JS,执行完后再恢复 DOM 解析。 defer 脚本: 异步下载,不阻塞 DOM 解析,等到 DOM 解析完全结束后,按照在 HTML 中的顺序依次执行。 async 脚本: 异步下载,不阻塞 DOM 解析,但只要下载完成,立刻暂停 DOM 解析并执行 JS(执行顺序无序)。 4. 生成渲染树(Render Tree)与布局(Layout) Render Tree 结合: 浏览器将 DOM 树和 CSSOM 树合并为 Render Tree。注意:display: none 的节点不会出现在渲染树中,而 visibility: hidden 的节点会。 布局计算(Layout/Reflow): 浏览器从根节点开始遍历渲染树,计算每个节点在屏幕上的确切几何位置、大小(即计算盒模型)。 5. 分层(Layering)与合成(Compositing) 现代浏览器为了提高性能,并不会把整个页面画在一张画布上,而是像 Photoshop 一样进行 分层(Layer) 。 拥有独立图层的属性:transform: translateZ(0)、will-change、position: fixed、拥有 3D 上下文的 <canvas> 等。 栅格化(Rastering): 每一个图层会被分割成许多“图块(Tiles)”,渲染进程中的栅格化线程池会将这些图块转换为位图(Bitmap)。通常会利用 GPU 加速 渲染。 6. 绘制(Painting)与显示 Paint: 浏览器记录每个图层的绘制指令(比如:先画背景,再画文字)。 Composit(合成): 浏览器主进程(Browser Process)接收到 GPU 进程生成的位图后,向显卡(GPU)发出指令,将各个图层合成,最终把像素投射到用户的屏幕上。
布莱克
2026-05-19 13:54浏览器已编辑
CDN加速
什么是CDN CDN(Content Delivery Network,内容分发网络) ,本质上是一个由遍布全球的边缘服务器节点组成的分布式网络。它的核心作用是: 将网站内容缓存到离用户最近的服务器上,让用户就近获取数据 。 工作流程: 当用户访问一个使用了 CDN 的网站时,流程是这样的: 用户请求资源:浏览器向网站域名发起请求 智能调度:CDN 的 DNS 系统判断用户的地理位置和网络状况 就近分配:系统将请求指向离用户最近的边缘节点 缓存响应:如果节点有缓存 → 直接返回(缓存命中)如果节点无缓存 → 从源站获取 → 缓存到节点 → 返回用户 资源类型 适合程度 原因 CSS/JS 文件 ⭐⭐⭐⭐⭐ 体积大、变更频率低、缓存收益高 图片/字体 ⭐⭐⭐⭐⭐ 不常变更、适合长期缓存 第三方库 (Vue/React) ⭐⭐⭐⭐⭐ 跨项目可共享缓存,命中率高 HTML 文件 ⭐⭐ 更新频繁、缓存策略难控制 API 响应 ⭐ 动态内容、不宜缓存 什么资源适合放 CDN? 资源类型 适合程度 原因 CSS/JS 文件 ⭐⭐⭐⭐⭐ 体积大、变更频率低、缓存收益高 图片/字体 ⭐⭐⭐⭐⭐ 不常变更、适合长期缓存 第三方库 (Vue/React) ⭐⭐⭐⭐⭐ 跨项目可共享缓存,命中率高 HTML 文件 ⭐⭐ 更新频繁、缓存策略难控制 API 响应 ⭐ 动态内容、不宜缓存 什么情况下依赖不随着webpack打包,放到cdn里更合适? CDN 的优势公式是: 收益 =(依赖体积 × 跨项目复用次数)- 额外请求开销 ✅ 强烈推荐使用 CDN 的场景 场景 为什么合适 典型例子 大型第三方库 体积大(>100KB),被全网大量站点共用,缓存命中率极高 Vue、React、ECharts、lodash、moment 企业多项目共享 同一用户在不同系统间切换时,依赖可复用缓存 公司所有后台共用同一套 UI 库 全球用户访问 CDN 边缘节点可大幅降低跨地域延迟 出海应用、跨境电商、国际化站点 源站带宽瓶颈 CDN 分担 80%+ 流量,降低服务器成本 高流量站点、文件下载服务 静态资源为主 图片、视频、字体等不常变更的资源,缓存收益最大 电商网站、内容平台、设计作品站 需要 DDoS 防护 CDN 边缘节点天然具备安全防护能力 金融、游戏、政务类网站 ❌ 不建议使用 CDN 的场景 场景 为什么不合适 典型例子 小型项目,主包 < 500KB 额外请求开销可能超过缓存收益,反而拖慢首屏 个人博客 、小型官网、静态展示页 依赖体积很小(<20KB) 单独发 HTTP 请求的成本高于直接打包 小型工具库、单函数工具 资源更新极其频繁 缓存命中率极低,CDN 形同虚设 实时数据接口、动态生成的内容 用户集中在同一区域 CDN 全球加速优势无法发挥,徒增复杂度 本地企业站、区域政务系统 内网/无法访问外网 CDN 完全不可用 企业内部系统、政务内网 安全合规限制 数据不能经过第三方节点 金融交易系统、医疗核心系统
布莱克
2026-05-18 20:54性能已编辑
发布-订阅模式
什么是发布订阅者模式 发布订阅者模式是一种 消息通信模式 ,它定义了对象间的一种 一对多的依赖关系 。当一个对象(发布者)的状态发生变化时,所有依赖于它的对象(订阅者)都会得到通知并自动更新 与观察者模式的区别 特性 观察者模式 发布订阅模式 耦合度 观察者知道被观察者存在 发布者和订阅者完全解耦 中间层 无 有事件通道/调度中心 通信方式 同步 通常异步 灵活性 较低 较高 核心组成: 事件中心:维护事件与回调函数的映射关系 发布者:触发事件 订阅者:监听事件并执行回调 待补充
布莱克
2026-05-15 16:54设计模式已编辑
React 和 Vue 对比
Vue 声明式 + 响应式编程 + 可变数据 Vue的核心心智模型是: 你修改数据,UI自动更新 // Vue的编程方式 const state = reactive({ count: 0 }) state.count++ // 直接修改,UI自动响应 // 你关注的是:修改数据这个动作 // 框架帮你处理:数据变了 -> 重新渲染哪些部分 精准订阅 (代理 + 响应追踪) 内部用 Proxy 拦截 data 的 getter/setter。 修改数据时,Vue 知道具体哪个组件、哪个属性依赖了这个数据。 更新时:只重新渲染依赖该数据的那个最小组件(非常精准)。 React 声明式 + 函数式编程 + 不可变数据 React的核心心智模型是: UI = f(state) ,即UI是状态的纯函数输出。 // React的编程方式 const [count, setCount] = useState(0) setCount(count + 1) // 你告诉React:新状态是什么 // React内部做的是: // 旧状态 -> 新状态 -> 重新执行整个组件函数 -> 生成新UI 重新执行整个组件函数 没有任何 Proxy 或依赖追踪。 setState 触发后,React 不知道该组件里哪些地方用了这个 state。 解决方案:重新执行整个组件函数,重新计算所有 JSX。 对比:React 的更新粒度是组件级别(虽然最终 diff 算法能复用大部分 DOM,但函数会从头跑一遍) Vue 的更新流程 简单、同步、确定性 修改数据 → 触发 setter 通知所有依赖的 Watcher 将 Watcher 放入微任务队列(Promise.then) 同一事件循环中批量执行 Watcher 每个 Watcher 执行 beforeUpdate → 生成新虚拟 DOM → diff → 更新真实 DOM 整个过程不可中断 React 的更新流程 Fiber + 优先级 + 可中断 setState → 创建更新对象,进入调度阶段(Scheduler) 根据优先级(用户点击 > 动画 > 网络请求)安排任务 协调阶段(Reconciliation):可中断:高优先级任务(如输入框事件)可以打断低优先级的更新低优先级更新可能被丢弃,等空闲时重新执行使用 双缓存 Fiber 树(current 树显示界面,workInProgress 树在内存中构建) 提交阶段(Commit):同步、不可中断将 workInProgress 树的变化应用到真实 DOM双树切换(新的 current 树指向 workInProgress) Vue的Dom更新是异步批量更新,react呢,每个setState都会触发更新吗? 在 React 中,多个 setState 默认情况下不会导致每个都触发一次组件函数重新执行,而是会合并成一次重新执行 React 在合成事件中批量更新(一次渲染),在异步代码中老版本会多次渲染,React 18 后基本都能批量 如何理解react的合成事件? 合成事件是 React 自己实现的一套跨浏览器的原生事件包装层。它并不是直接绑定到具体的 DOM 元素上,而是统一绑定在根容器上,采用事件代理机制 React 在组件渲染时,不会给每个 button 单独绑定 click 事件 React 在根容器(如 document 或 root 节点)上只绑定一个事件监听器 当用户点击按钮,真实 DOM 事件冒泡到根容器 React 根据事件的 target 找到对应的虚拟 DOM 和组件 React 创建合成事件对象(SyntheticEvent)并调用你写的 handleClick 什么是Fiber 待学习 Hook 让你从一个普通函数(组件函数)内部,“伸出一个钩子”去 钩住 React的核心功能(状态、副作用、上下文等),然后把这些功能“拉”到你的组件里使用 function MyComponent() { // 用 useState 这个“钩子”,钩住 React 内部的状态管理 const [count, setCount] = useState(0) // 用 useEffect 这个“钩子”,钩住 React 的渲染生命周期 useEffect(() => { document.title = `点击了${count}次` }, [count]) return <button onClick={() => setCount(count+1)}>点我</button> } React Hook 作用 Vue 3 对应 useState 声明响应式状态 ref / reactive useEffect 处理副作用(DOM操作、请求、定时器) watchEffect / watch useContext 获取跨层级数据 inject useRef 保存一个不触发重新渲染的值 / DOM引用 ref (同名的,注意区分) useMemo 缓存计算结果 computed useCallback 缓存函数引用 没有直接对应,但Vue不需要(因为函数不会无故重新创建) useReducer 复杂状态逻辑(类似Redux) 通常用 reactive + 方法
布莱克
2026-05-15 10:48React已编辑
CommonJS规范 ESM规范
require: CommonJS规范,最初用于Node.js环境 import: ES6模块规范,JavaScript官方标准 维度 require import 规范 CommonJS ES Module 加载时机 运行时 (代码执行到才加载) 编译时 (代码执行前就解析) 位置限制 任何位置 必须在模块顶部 动态路径 ✅ 支持 ❌ 不支持(需 import()) 值的性质 值的拷贝 实时只读引用 Tree Shaking ❌ 不支持 ✅ 支持 // require - CommonJS 规范 const module = require('./module') const { fn1, fn2 } = require('./module') // import - ES Module 规范 import module from './module' import { fn1, fn2 } from './module' 加载时机: require是运行时加载,代码执行到那一行才加载 import是编译时加载,代码执行前就已经加载解析 // require - 运行时加载 console.log('start') const math = require('./math') // 执行到这里才会加载 console.log(math.add(1, 2)) // import - 静态加载(编译时) import { add } from './math' // 代码执行前就已完成加载和解析 console.log('start') console.log(add(1, 2)) require的优势和不足: ✅ 优势: // 1. 真正的动态加载,节省资源 if (feature.isEnabled) { const feature = require('./feature') // 不启用就不加载 } // 2. 适合大型应用的分层加载 const middleware = [] if (env === 'dev') { middleware.push(require('./dev-tools')) } // 3. 配置文件的天然支持 const config = require(`./config/${NODE_ENV}.json`) // 4. 运行时修改模块(热替换) delete require.cache[moduleId] const newModule = require(moduleId) // 重新加载 ❌ 不足: // 1. 无法 Tree Shaking // 即使只用到 one 函数,整个 utils 都会打包 const utils = require('./utils') utils.one() // 2. 同步阻塞 const hugeLib = require('./huge-lib') // 卡住直到加载完成 // 3. 无法静态分析 // 打包工具不知道哪些模块被使用了 // 导致代码分割困难 // 4. 循环依赖可能产生不完整对象 // a.js const b = require('./b') module.exports = { value: 1, b } // 此时 b 可能还是空对象 import的优势和不足: ✅ 优势: // 1. Tree Shaking - 只打包使用的代码 import { debounce } from 'lodash-es' // 只会打包 debounce,其他函数被 tree-shake 掉 // 2. 异步加载(无需额外配置) const module = await import('./heavy-module') // 3. 更容易做代码分割 // Webpack/Vite 能根据 import 自动分割 chunk const AdminPage = lazy(() => import('./admin/AdminPage')) // 4. 更好的静态分析 // IDE 能准确知道依赖关系,提供智能提示 // 可以做类型推导、自动导入等高级特性 // 5. 更容易实现变量绑定(实时引用) // 导出的是引用,不是值拷贝 ❌ 不足: // 1. 动态能力弱(需要特殊语法) // 不能直接写变量路径 import('./locale/' + lang + '.json') // 能工作但有限制 // 打包工具会生成多个 chunk,可能不是你想要的 // 2. 严格的循环依赖检测(可能误杀合法场景) // a.mjs import { b } from './b.mjs' export const a = () => b() // 3. 加载时机固定 // 即使后面没用到,也会加载执行 import hugeLib from './huge-lib' // 一定会加载 // 4. 需要在模块顶层使用 if (true) { import { x } from './x' // ❌ 语法错误 } // 只能用 import() 动态导入,但那是异步的 为什么CommonJS不能实现摇树优化: Tree Shaking 的本质是 编译时静态分析 ,删除未使用的代码。但 CommonJS 的模块结构是 动态的 require 是 运行时加载 ,代码执行到 require 语句时才去读取、解析、执行模块。这给了它很大的灵活性——可以在任何位置调用,可以使用动态路径,可以根据条件加载。但打包工具无法做静态分析, 不支持 Tree Shaking ,因为模块的导出结构可能在运行时发生变化。
布莱克
2026-05-11 16:36JavaScript
Vue组件通信
Vue2组件通信方式: Props / $emit (父子组件通信) // 父组件 <template> <Child :message="parentMsg" @childEvent="handleEvent" /> </template> <script> export default { data() { return { parentMsg: 'Hello from parent' } }, methods: { handleEvent(data) { console.log('收到子组件事件:', data) } } } </script> // 子组件 <script> export default { props: ['message'], methods: { sendToParent() { this.$emit('childEvent', '数据来自子组件') } } } </script> provide / inject (跨层级传递) //provide/inject 默认不是响应式的,需要传递响应式对象才能实现响应式。 // 祖先组件 export default { provide() { return { userInfo: this.userInfo, updateUser: this.updateUser } }, data() { return { userInfo: { name: '张三' } } }, methods: { updateUser(name) { this.userInfo.name = name } } } // 后代组件 export default { inject: ['userInfo', 'updateUser'], mounted() { console.log(this.userInfo) this.updateUser('李四') } } Event Bus (事件总线) // EventBus 对象本质上就是一个普通的 Vue 实例,没有 template等等,但拥有 Vue 实例的所有能力 // event-bus.js import Vue from 'vue' export const EventBus = new Vue() // 组件 A(发送事件) import { EventBus } from './event-bus' EventBus.$emit('custom-event', { data: 'some data' }) // 组件 B(监听事件) import { EventBus } from './event-bus' EventBus.$on('custom-event', (payload) => { console.log(payload) }) // 监听事件组件销毁前移除监听 beforeDestroy() { EventBus.$off('custom-event') } 为什么 EventBus 能跨组件通信? 因为 A 和 B 导入的是同一个对象(单例模式) 所以它们操作的是同一个事件中心 EventBus 本质上是一个 全局的发布-订阅对象 ,它会持久存在(除非页面刷新或关闭)。当一个组件监听了一个事件后,EventBus 内部会持有该监听函数的引用。 发送事件的组件:只是调用 $emit 触发事件,EventBus 不持有该组件的任何引用,组件销毁不影响 EventBus 监听事件的组件:通过 $on 将回调函数注册到 EventBus 上,EventBus 持有这个回调函数的引用,如果回调函数中访问了组件的数据,就会形成引用链 如果不移除会有什么后果? 内存泄漏 当组件 B 被销毁(如路由切换、v-if 隐藏)后: 组件 B 虽然从 DOM 中移除了 但 EventBus 仍然持有回调函数的引用 回调函数通过闭包持有组件 B 的 this 引用 组件 B 及其所有数据(包括 hugeData)永远不会被垃圾回收 重复监听导致逻辑错误 //用户反复进入/离开组件多次 // 组件 mounted() { EventBus.$on('update-count', () => { this.count++ // 每次进入组件都会执行多次 }) } // 场景:用户进入页面 → 离开 → 重新进入 → 点击按钮触发事件 // 点击一次,count 会增加 3 次(因为注册了 3 个监听器) 组件销毁后仍然执行逻辑 Vuex (全局状态管理) // store.js export default new Vuex.Store({ state: { count: 0 }, mutations: { increment(state) { state.count++ } }, actions: { incrementAsync({ commit }) { setTimeout(() => commit('increment'), 1000) } }, getters: { doubleCount: state => state.count * 2 } }) // 组件中使用 this.$store.commit('increment') this.$store.dispatch('incrementAsync') this.$store.getters.doubleCount Vue3组件通信: Props / $emit (变化较小) <!-- 子组件 Child.vue --> <script setup> // 定义 props const props = defineProps({ message: String, count: Number }) // 定义 emits const emit = defineEmits(['update', 'childEvent']) const sendToParent = () => { emit('childEvent', '数据来自子组件') } </script> <!-- 父组件 --> <template> <Child :message="msg" @childEvent="handleEvent" /> </template> <script setup> import { ref } from 'vue' import Child from './Child.vue' const msg = ref('Hello from parent') const handleEvent = (data) => { console.log(data) } </script> 使用 mitt 实现 Event Bus Vue 3 专注于组件核心功能,EventBus 属于“模式”而非“核心功能” // event-bus.js import mitt from 'mitt' export const emitter = mitt() // 组件 A(发送事件) import { emitter } from './event-bus' emitter.emit('custom-event', { data: 'some data' }) // 组件 B(监听事件) import { emitter } from './event-bus' import { onUnmounted } from 'vue' const handler = (payload) => { console.log(payload) } emitter.on('custom-event', handler) // 组件卸载时移除监听 onUnmounted(() => { emitter.off('custom-event', handler) }) Vuex 4 / Pinia (状态管理) // stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ name: '张三', age: 18 }), getters: { adult: (state) => state.age >= 18 }, actions: { updateName(name) { this.name = name } } }) // 组件中使用 <script setup> import { useUserStore } from '@/stores/user' import { storeToRefs } from 'pinia' const userStore = useUserStore() // 使用 storeToRefs 保持响应性 const { name, age, adult } = storeToRefs(userStore) const { updateName } = userStore // 直接调用 action updateName('李四') </script>
布莱克
2026-05-10 15:49Vue已编辑
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 9
assistant