浏览器相关 & 函数式编程
浏览器 DOM/BOM、事件模型、ajax、跨域、RESTful、渲染流程、性能优化、函数式编程原理
目录
| 模块 | 章节 | 核心内容 |
|---|---|---|
| Part I | 浏览器相关 | 浏览器处理模型、对象模型、事件模型、防抖与节流 |
| Part II | 函数式编程 | 函数式原理、流水线组装 |
| Part III | 网络层 | ajax、跨域、RESTful、状态码 |
| Part IV | 浏览器底层原理 | DOM/CSSOM、渲染流程、性能优化 |
Part I. 浏览器相关
一、前端主要舞台 —— 浏览器
1. 浏览器处理模型:DOM、BOM、BEM
浏览器是前端代码运行的宿主环境,理解它的处理模型是入门基础。
| 模型 | 全称 | 作用 |
|---|---|---|
| DOM | Document Object Model | 文档对象模型,操作 HTML 结构 |
| BOM | Browser Object Model | 浏览器对象模型,操作浏览器窗口 |
| BEM | Block Element Modifier | CSS 命名规范(非浏览器模型,但常一并讨论) |
// DOM 操作:修改文档结构
document.querySelector('#app').innerHTML = 'Hello'
// BOM 操作:控制浏览器行为
window.location.href = 'https://example.com' // 跳转页面
window.history.back() // 后退
// BEM 命名:CSS 类名规范
// .block__element--modifier
// .card__title--large
2. 从一道经典面试题开始
💡 经典面试题: 从输入 URL 到页面展示,中间发生了什么?
这道题串起了几乎所有前端知识点:
- DNS 解析 → TCP 连接 → HTTP 请求
- 服务器响应 → 浏览器解析 → 渲染页面
- JS 执行 → 事件绑定 → 交互响应
二、对象模型和事件模型
3. 对象模型
3.1 全局管家,主入口 —— window
window 是浏览器中所有对象的根,全局变量和函数都挂载在它身上。
// 全局变量本质上是 window 的属性
var name = 'Shea'
console.log(window.name) // 'Shea'
// 常用 window API
window.alert('提示')
window.setTimeout(() => {}, 1000)
window.innerWidth // 视口宽度
⚠️ 注意:
let和const声明的变量不会挂载到 window 上。
3.2 专业导航,确保不迷路 —— location & history
// location:当前页面的 URL 信息
location.href // 完整 URL
location.protocol // 协议(http:/https:)
location.host // 主机名 + 端口
location.pathname // 路径
location.search // 查询字符串(?key=value)
location.hash // 锚点(#section)
// history:浏览历史栈
history.back() // 后退
history.forward() // 前进
history.go(-2) // 跳转到前 2 页
history.pushState({}, '', '/new-path') // 添加历史记录(SPA 路由核心)
history.replaceState({}, '', '/new-path') // 替换当前记录
3.3 更多信息的备忘录 —— navigation、screen……
// navigator:浏览器和系统信息
navigator.userAgent // 用户代理字符串
navigator.language // 浏览器语言
navigator.onLine // 是否在线
// screen:屏幕信息
screen.width // 屏幕宽度
screen.height // 屏幕高度
screen.availWidth // 可用宽度(去除任务栏)
💡 面试中常见的对象 API 相关问题: 考察对 BOM 的整体认知,而不只是死记 API。
4. 事件模型
4.1 常规流:捕获 => 目标 => 冒泡 vs 人工阻断
事件传播三阶段:
| 阶段 | 方向 | 说明 |
|---|---|---|
| 捕获阶段 | window → target | 从外向内传递 |
| 目标阶段 | target | 到达目标元素 |
| 冒泡阶段 | target → window | 从内向外冒泡 |
// addEventListener 第三个参数控制捕获/冒泡
element.addEventListener('click', handler, true) // 捕获阶段触发
element.addEventListener('click', handler, false) // 冒泡阶段触发(默认)
// 人工阻断
function handler(e) {
e.stopPropagation() // 阻止冒泡/捕获继续传播
e.stopImmediatePropagation() // 阻止同元素其他监听器
e.preventDefault() // 阻止默认行为(如表单提交)
}
4.2 事件绑定
① 原理
事件绑定本质是:把回调函数登记到 DOM 节点的事件监听列表中,当事件触发时按顺序执行。
// 三种绑定方式对比
// 方式 1:HTML 内联(不推荐)
// <button onclick="handleClick()">
// 方式 2:DOM 属性赋值(同一事件只能绑一个)
btn.onclick = function() { console.log('clicked') }
// 方式 3:addEventListener(推荐,可绑多个)
btn.addEventListener('click', handler1)
btn.addEventListener('click', handler2)
② 面试核心
兼容相关 —— 事件绑定的浏览器兼容
// 现代 vs 老 IE 兼容写法
function addEvent(el, type, handler) {
if (el.addEventListener) {
// 标准浏览器
el.addEventListener(type, handler, false)
} else if (el.attachEvent) {
// 老 IE(IE8 及以下),事件名要加 'on'
el.attachEvent('on' + type, handler)
} else {
// 最兜底
el['on' + type] = handler
}
}
性能相关 —— 多子节点的事件绑定优化
💡 核心思路: 事件委托(Event Delegation)—— 利用冒泡,把子节点的监听器统一挂在父节点上。
// ❌ 反例:每个 li 都绑一个事件,性能差
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', () => console.log(li.textContent))
})
// ✅ 正例:事件委托,只绑一次
document.querySelector('ul').addEventListener('click', e => {
// 通过 e.target 判断真正点击的元素
if (e.target.tagName === 'LI') {
console.log(e.target.textContent)
}
})
🚨 委托的好处: ① 减少内存占用 ② 动态添加的子节点也能响应事件
5. 防抖(debounce)与节流(throttle)
两者都是限制函数执行频率的优化手段,专门应对 scroll、resize、input 等高频触发事件。
防抖(debounce)
核心思想: 事件持续触发时不执行,停止触发后等待 n 毫秒才执行一次。
电梯类比: 只要有人按按钮,就重新倒计时,直到无人再按才关门启动。
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer); // 每次触发都重置计时器
timer = setTimeout(() => {
fn.apply(this, args); // delay 后无新触发,才真正执行
}, delay);
};
}
// 使用:搜索框输入联想(用户停止输入 500ms 后才发请求)
const handleSearch = debounce((keyword) => {
fetch(`/api/search?q=${keyword}`);
}, 500);
input.addEventListener('input', e => handleSearch(e.target.value));
节流(throttle)
核心思想: 持续触发时,每隔 n 毫秒只执行一次,保证规律性执行。
水龙头类比: 控制流量,每隔固定时间放一次水,不管旋钮拧多猛。
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) { // 距上次执行超过间隔才执行
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用:页面滚动加载更多(每 200ms 最多检查一次)
const handleScroll = throttle(() => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMore(); // 触底加载
}
}, 200);
window.addEventListener('scroll', handleScroll);
立即执行版防抖(leading debounce)
有时需要第一次触发立即执行,冷却期内的高频触发被忽略:
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (immediate && !timer) {
fn.apply(this, args); // 第一次立即执行
}
clearTimeout(timer);
timer = setTimeout(() => {
timer = null; // 冷却结束,重置标志
if (!immediate) fn.apply(this, args);
}, delay);
};
}
对比总结
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行时机 | 停止触发后才执行(trailing) | 每隔固定时间执行一次 |
| 高频中是否执行 | ❌ 不执行,一直重置 | ✅ 按频率执行 |
| 典型场景 | 搜索联想、表单验证、resize 结束后计算布局 | 滚动加载、mousemove、游戏帧更新 |
| 核心操作 | 清除并重新计时 | 比较时间差 |
Part II. 函数式编程
一、函数式编程的出现
发展历程
| 阶段 | 范式 | 核心思想 |
|---|---|---|
| 1 | 命令(脚本)式编程 | 一步步告诉计算机怎么做 |
| 2 | 面向对象编程 | 用对象封装数据和行为 |
| 3 | 函数式编程 | 用函数作为主角,组合而非继承 |
// 1. 命令式:关注"怎么做"
const nums = [1, 2, 3, 4, 5]
const doubled1 = []
for (let i = 0; i < nums.length; i++) {
doubled1.push(nums[i] * 2)
}
// 2. 面向对象:用对象组织
class Doubler {
constructor(arr) { this.arr = arr }
run() { return this.arr.map(n => n * 2) }
}
// 3. 函数式:关注"做什么",声明意图
const double = n => n * 2
const doubled3 = nums.map(double)
💡 函数式的主角: 函数(Function)—— 一等公民,可传递、可返回、可组合。
二、函数式编程原理特点
1. 函数式原理
核心原则:
- 纯函数: 相同输入永远得到相同输出,且无副作用
- 不可变性: 不修改原数据,而是返回新数据
- 函数组合: 小函数拼装成大功能
// ❌ 非纯函数:依赖外部变量,有副作用
let count = 0
function impureAdd(n) {
count += n // 修改了外部状态
return count
}
// ✅ 纯函数:输入决定输出,无副作用
function pureAdd(a, b) {
return a + b
}
// ✅ 不可变性:不改原数组
const arr = [1, 2, 3]
const newArr = [...arr, 4] // 创建新数组,而非 arr.push(4)
2. 理论思想 —— 一个中心(函数)两个基本点(函数功能 + 流程串联)
💡 一句话概括: 用函数封装功能,用组合串联流程。
// 一个中心:函数
const trim = str => str.trim()
const toLower = str => str.toLowerCase()
const replaceSpace = str => str.replace(/\s+/g, '-')
// 两个基本点:
// ① 函数功能(单一职责)
// ② 流程串联(组合)
const slugify = str => replaceSpace(toLower(trim(str)))
console.log(slugify(' Hello World ')) // 'hello-world'
3. 函数式编程的主角 —— 惰性执行
惰性执行(Lazy Evaluation):不立即计算结果,而是在真正需要时才执行。
// 惰性执行示例:链式调用直到 .value() 才真正计算
function lazy(value) {
const operations = []
return {
map(fn) {
operations.push(fn) // 只是记录操作,不执行
return this
},
value() {
// 真正用到结果时才依次执行
return operations.reduce((acc, fn) => fn(acc), value)
}
}
}
const result = lazy(5)
.map(x => x * 2) // 不执行
.map(x => x + 1) // 不执行
.value() // 此时才执行:5 -> 10 -> 11
console.log(result) // 11
⚠️ 注意: 惰性执行能避免不必要的计算,是函数式性能优化的重要手段。
三、流水线的组装
1. 流水线式编程
流水线编程:把数据像传送带上的产品一样,经过一道道函数加工,最后得到成品。
// 数组方法天然就是流水线
const result = [1, 2, 3, 4, 5]
.filter(n => n % 2 === 0) // 过滤偶数:[2, 4]
.map(n => n * n) // 平方:[4, 16]
.reduce((sum, n) => sum + n, 0) // 求和:20
console.log(result) // 20
2. 加工与变化
a. 过程加工
每一步函数都是一道工序,对数据进行转换、过滤、聚合。
// 通用流水线工具:pipe
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)
const addOne = x => x + 1
const double = x => x * 2
const square = x => x * x
// 组装流水线:先 +1,再 ×2,再平方
const process = pipe(addOne, double, square)
console.log(process(3)) // ((3+1)*2)^2 = 64
b. 函子(Functor)
函子是函数式编程的高阶概念:一个包含值的容器,且能用 map 对值进行操作而不离开容器。
// 简单 Functor 实现
class Container {
constructor(value) {
this._value = value
}
// 静态方法,方便构造
static of(value) {
return new Container(value)
}
// map:在容器内对值进行操作,返回新容器
map(fn) {
return Container.of(fn(this._value))
}
}
const result = Container.of(5)
.map(x => x + 1) // Container(6)
.map(x => x * 2) // Container(12)
console.log(result) // Container { _value: 12 }
💡 提示: Array、Promise 本质都是函子 —— 它们都有 map / then 方法,把值包在容器里安全处理。
Part III. 网络层
5. ajax 的原理与手写 ajax 请求
AJAX(Asynchronous JavaScript And XML):浏览器在不刷新页面的前提下,与服务器进行数据交换。
核心 API: XMLHttpRequest(老)/ fetch(新)
手写一个 ajax 请求(基于 XHR)
function ajax({ url, method = 'GET', data = null, headers = {} }) {
return new Promise((resolve, reject) => {
// 1. 创建 XHR 实例
const xhr = new XMLHttpRequest()
// 2. 初始化请求(第三个参数 true 表示异步)
xhr.open(method, url, true)
// 3. 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key])
})
// 4. 监听状态变化
xhr.onreadystatechange = function() {
// readyState === 4 表示请求完成
if (xhr.readyState !== 4) return
// 状态码 2xx 或 304 表示成功
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`请求失败:${xhr.status}`))
}
}
// 5. 发送请求
xhr.send(data ? JSON.stringify(data) : null)
})
}
// 使用
ajax({ url: '/api/user', method: 'GET' })
.then(res => console.log(res))
.catch(err => console.error(err))
XHR 的 readyState 五个阶段
| 值 | 状态 | 含义 |
|---|---|---|
| 0 | UNSENT | 未调用 open() |
| 1 | OPENED | 已调用 open(),未发送 |
| 2 | HEADERS_RECEIVED | 已发送,响应头已接收 |
| 3 | LOADING | 响应体下载中 |
| 4 | DONE | 全部完成 |
6. 跨域问题
同源策略
🚨 同源策略: 协议、域名、端口三者完全相同才算同源,否则浏览器会拦截请求响应。
| URL 对比 | 是否同源 | 原因 |
|---|---|---|
https://a.com/x vs https://a.com/y |
✅ | 完全相同 |
https://a.com vs http://a.com |
❌ | 协议不同 |
https://a.com vs https://b.com |
❌ | 域名不同 |
https://a.com:80 vs https://a.com:8080 |
❌ | 端口不同 |
常见跨域解决方案
| 方案 | 原理 | 适用场景 |
|---|---|---|
| CORS | 服务端设置 Access-Control-Allow-Origin |
现代标准方案 |
| JSONP | 利用 <script> 标签不受同源限制 |
老项目,只支持 GET |
| 代理服务器 | 同源服务器转发请求 | 开发环境最常用 |
| postMessage | 跨窗口通信 | iframe 通信 |
// JSONP 原理简单实现
function jsonp(url, callback) {
const cbName = 'jsonp_' + Date.now()
// 把回调挂到全局
window[cbName] = function(data) {
callback(data)
document.body.removeChild(script)
delete window[cbName]
}
// 用 script 标签绕过同源策略
const script = document.createElement('script')
script.src = `${url}?callback=${cbName}`
document.body.appendChild(script)
}
// CORS:服务端响应头(Node.js 示例)
res.setHeader('Access-Control-Allow-Origin', '*') // 允许的源
res.setHeader('Access-Control-Allow-Methods', 'GET,POST') // 允许的方法
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
7. RESTful 与 ajax 的多种请求方式
RESTful 是一种 API 设计风格:用 HTTP 方法表达操作,用 URL 表达资源。
| HTTP 方法 | 语义 | 示例 |
|---|---|---|
| GET | 查询资源 | GET /posts 获取文章列表 |
| POST | 新建资源 | POST /posts 创建文章 |
| PUT | 全量更新 | PUT /posts/1 替换文章 1 |
| PATCH | 部分更新 | PATCH /posts/1 修改部分字段 |
| DELETE | 删除资源 | DELETE /posts/1 删除文章 1 |
// 用 fetch 实现 RESTful 风格的调用
// GET 查询
fetch('/api/posts').then(r => r.json())
// POST 新建
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello', content: '...' })
})
// PUT 更新
fetch('/api/posts/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Updated' })
})
// DELETE 删除
fetch('/api/posts/1', { method: 'DELETE' })
💡 RESTful 设计原则: URL 用名词复数(/posts 而非 /getPosts),方法表达动作,响应用状态码表达结果。
8. 从状态码开始,到浏览器存储
HTTP 状态码分类
| 范围 | 类别 | 常见状态码 |
|---|---|---|
| 1xx | 信息 | 100 Continue |
| 2xx | 成功 | 200 OK / 201 Created / 204 No Content |
| 3xx | 重定向 | 301 永久 / 302 临时 / 304 Not Modified(协商缓存) |
| 4xx | 客户端错误 | 400 请求错误 / 401 未授权 / 403 禁止 / 404 Not Found |
| 5xx | 服务端错误 | 500 内部错误 / 502 网关错误 / 503 不可用 |
浏览器存储对比
| 存储方式 | 容量 | 生命周期 | 与服务器通信 | 适用场景 |
|---|---|---|---|---|
| cookie | ~4KB | 可设置过期 | 每次请求自动携带 | 登录态、SessionID |
| localStorage | ~5MB | 永久(除非手动清除) | 不参与 | 长期偏好设置 |
| sessionStorage | ~5MB | 标签页关闭即清除 | 不参与 | 表单临时数据 |
| IndexedDB | 几百 MB | 永久 | 不参与 | 大量结构化数据、离线应用 |
// cookie:document.cookie 操作较繁琐
document.cookie = 'token=abc123; max-age=3600; path=/'
// localStorage / sessionStorage API 完全一致
localStorage.setItem('user', JSON.stringify({ name: 'Shea' }))
const user = JSON.parse(localStorage.getItem('user'))
localStorage.removeItem('user')
localStorage.clear()
sessionStorage.setItem('draft', '草稿内容')
⚠️ 注意: cookie 会被每个请求携带,过多会影响性能;敏感数据应配合
HttpOnly和Secure标记。
Part IV. 浏览器底层原理
9. 概念:DOM、CSSOM、RenderTree、Layout、Painting
| 概念 | 全称 | 作用 |
|---|---|---|
| DOM | Document Object Model | HTML 解析后的节点树 |
| CSSOM | CSS Object Model | CSS 解析后的样式树 |
| RenderTree | 渲染树 | DOM + CSSOM 合并(只含可见节点) |
| Layout | 布局(回流/Reflow) | 计算每个节点的位置和大小 |
| Painting | 绘制(重绘/Repaint) | 把节点画到屏幕像素上 |
10. 页面渲染流程
浏览器渲染五步走:
HTML ──┐
├─→ DOM Tree ──┐
CSS ──┴─→ CSSOM ─────┴─→ RenderTree ─→ Layout ─→ Painting ─→ 屏幕显示
详细流程:
- 解析 HTML: 字节流 → 字符 → Token → Node → DOM Tree
- 解析 CSS: 同理构建 CSSOM Tree
- 合成 RenderTree: 遍历 DOM,跳过不可见节点(display:none、script、meta),为每个可见节点匹配 CSSOM 规则
- Layout 布局: 计算节点的精确位置和大小(box model)
- Painting 绘制: 分层 → 栅格化 → 合成 → 显示
11. 结合渲染流程铺开 —— DOM / CSSOM 的创建过程
DOM 创建
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
// 上述 HTML 解析后形成的 DOM Tree(简化结构):
// html
// └── body
// ├── h1
// │ └── "Hello"
// └── p
// └── "World"
CSSOM 创建
body { font-size: 16px; }
h1 { color: red; font-size: 32px; }
p { color: blue; }
⚠️ CSSOM 的特点: CSS 是"渲染阻塞"资源,必须等 CSSOM 构建完成才能合成 RenderTree。
🚨 重要警告: JS 既会阻塞 DOM 解析,又会等待 CSSOM(因为可能查询样式)。所以 JS 推荐放在
</body>之前,或使用defer/async。
<!-- 三种 script 加载方式对比 -->
<script src="a.js"></script> <!-- 阻塞解析,立即下载执行 -->
<script src="b.js" async></script> <!-- 异步下载,下载完立即执行(乱序) -->
<script src="c.js" defer></script> <!-- 异步下载,DOM 解析完才执行(有序) -->
| 加载方式 | 下载 | 执行时机 | 顺序保证 |
|---|---|---|---|
| 默认 | 阻塞 | 立即 | ✅ |
| async | 异步 | 下载完立即(可能在 DOMContentLoaded 前后) | ❌ |
| defer | 异步 | DOMContentLoaded 前按顺序 | ✅ |
12. 回到最初的面试题
💡 经典面试题回归: 从输入 URL 到页面展示,中间发生了什么?
结合前面所学,完整答案应包含:
- URL 解析: 解析协议、域名、路径
- DNS 查询: 域名 → IP(浏览器缓存 → 系统缓存 → 路由器 → ISP → 根域名服务器)
- TCP 三次握手: 建立连接(HTTPS 还要 TLS 握手)
- HTTP 请求: 发送 Request
- 服务器处理 & 响应: 返回 Response
- 浏览器接收: 解析 HTML → 构建 DOM
- 加载子资源: CSS / JS / 图片(可能复用 TCP 连接)
- 构建 CSSOM、执行 JS
- 合成 RenderTree → Layout → Painting
- TCP 四次挥手: 断开连接
13. 了解浏览器机制的目的 => 性能优化
💡 核心目的: 了解原理,是为了知道在哪里"省"和怎么"省",从而做性能优化。
优化方向一:减少回流(Reflow)与重绘(Repaint)
回流一定引起重绘,重绘不一定引起回流。回流的代价远高于重绘。
| 操作 | 触发 |
|---|---|
| 修改宽高、位置、字体大小 | 回流 + 重绘 |
| 修改颜色、背景、可见性 | 仅重绘 |
transform、opacity |
合成层,不触发回流重绘 |
// ❌ 反例:每次修改样式都触发回流
const el = document.getElementById('box')
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
// ✅ 正例 1:合并样式修改(用 class)
el.className = 'new-style'
// ✅ 正例 2:使用 cssText 批量赋值
el.style.cssText = 'width:100px; height:100px; margin:10px'
// ✅ 正例 3:离线 DOM 操作
el.style.display = 'none' // 脱离文档流
// ... 多次操作
el.style.display = 'block' // 一次性回流
优化方向二:资源加载优化
- CSS: 放在
<head>,避免无样式闪烁(FOUC) - JS: 放在
</body>前,或用defer/async - 图片: 懒加载、压缩、使用 WebP/AVIF
- 字体: 使用
font-display: swap避免阻塞
优化方向三:利用缓存
// HTTP 缓存策略
// 强缓存:Cache-Control: max-age=3600
// 协商缓存:If-Modified-Since / ETag(对应 304 状态码)
// 业务层:Service Worker / IndexedDB 离线缓存
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
优化方向四:善用合成层
/* 启用 GPU 加速,绕过 Layout 和 Paint */
.smooth-anim {
transform: translateZ(0); /* 或 will-change: transform */
transition: transform 0.3s;
}
🚨 警告: 合成层不是越多越好,过多合成层会占用大量内存,反而拖慢页面。
📌 总结思维导图
JS 核心知识
│
├── Part I 浏览器相关
│ ├── 处理模型:DOM / BOM / BEM
│ ├── 对象模型:window / location / history / navigator
│ └── 事件模型:三阶段 + 绑定 + 委托
│
├── Part II 函数式编程
│ ├── 演进:命令式 → 面向对象 → 函数式
│ ├── 三大原则:纯函数 / 不可变 / 组合
│ └── 流水线:pipe / Functor / 惰性执行
│
├── Part III 网络层
│ ├── ajax:XHR / fetch
│ ├── 跨域:CORS / JSONP / 代理
│ ├── RESTful:GET/POST/PUT/PATCH/DELETE
│ └── 状态码 + 浏览器存储
│
└── Part IV 浏览器底层
├── 五大概念:DOM/CSSOM/RenderTree/Layout/Paint
├── 渲染流程 + 阻塞机制(CSS/JS)
└── 性能优化:减少回流 / 资源加载 / 缓存 / 合成层