一、异步
1. 回调函数
回调函数是最早用来处理异步操作的办法,不过它有个大问题,容易写出回调地狱,代码可读性和维护性都变得很差。
2. Promise
ES6 引入的 Promise 解决了回调地狱这个麻烦。then 方法会返回一个新的 Promise,它的状态是根据上一个 Promise 的状态来定的。
function A() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('yes');
}, 1000);
});
}
function B() {
console.log('B');
}
A().then(() => B()); // yes B
3. Generator
Generator 函数靠 * 和 yield 关键字来实现异步控制,它返回一个迭代器对象。
迭代器(Iterator)是一种特殊对象,它提供了一种统一的方式来遍历可迭代对象(如数组、字符串、Set、Map 等)中的元素。迭代器对象有一个
next()方法,每次调用该方法时,它会返回一个包含两个属性的对象:value和done。value是当前迭代的值,done是一个布尔值,表示迭代是否结束。
// 定义一个 Generator 函数 foo,接收一个参数 x
function *foo(x) {
// 第一次调用 yield,暂停函数执行,并返回 x + 1 的值
let y = 2 * (yield (x + 1));
// 第二次调用 yield,暂停函数执行,并返回 y / 3 的值
let z = yield (y / 3);
// 返回最终结果 x + y + z
return (x + y + z);
}
// 调用 Generator 函数,返回一个迭代器对象 it
let it = foo(5);
// 第一次调用 next() 方法,开始执行 Generator 函数,直到遇到第一个 yield
// 返回 { value: 6, done: false },value 是 yield 后面表达式的值,done 表示迭代未结束
console.log(it.next());
// 第二次调用 next() 方法,继续执行 Generator 函数,将传入的参数 12 赋给上一次 yield 的返回值
// 计算 y = 2 * 12 = 24,然后遇到第二个 yield,返回 { value: 8, done: false }
console.log(it.next(12));
// 第三次调用 next() 方法,继续执行 Generator 函数,将传入的参数 13 赋给上一次 yield 的返回值
// 计算 z = 13,然后执行 return 语句,返回 { value: 42, done: true },done 表示迭代结束
console.log(it.next(13));
在这个示例中,我们定义了一个 Generator 函数 foo,并调用它得到一个迭代器对象 it。通过多次调用 it.next() 方法,我们可以逐步执行 foo 函数中的代码,并在每次遇到 yield 时暂停执行。每次调用 next() 方法时传入的参数会作为上一次 yield 表达式的返回值。最终,当 Generator 函数执行完毕时,next() 方法返回的对象的 done 属性为 true,表示迭代结束
。
4. Async/Await
async/await其实是基于Promise的一种异步编程语法糖,用它写异步代码,看起来就跟同步代码差不多。
// 定义一个异步函数 A
async function A() {
// 等待异步函数 B 执行完毕
await B();
// 当 B 函数执行完毕后,打印 2
console.log(2);
}
// 定义一个异步函数 B
async function B() {
console.log(3);
}
// 调用异步函数 A
A(); // 3 2
二、Event Loop
-
线程之间可以同时工作,除了 js 线程 和 浏览器渲染线程 。因为 js 可以修改 dom,可能会出现渲染冲突(js 的执行会阻塞 html)
-
同步队列:
-
异步队列:
- 宏任务队列:同步代码的第一次执行也叫宏任务
- 微任务队列: promise.then 、 MutationObserver(监听dom结构) 、 node环境独有:process 、 nextTick
JavaScript 是单线程的,得靠 Event Loop 来实现异步操作。执行顺序是这样的:
- 先执行同步代码(这属于宏任务)。
- 然后处理微任务队列里的任务。
- 要是有必要,就执行渲染。
- 接着再处理宏任务。
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
Promise.resolve().then(() => {
console.log('6');
});
}, 0);
});
console.log('7');
// 输出顺序:1 7 4 2 3 5 6
三、浏览器基础考点
1. 事件机制
事件传播三阶段:
捕获阶段(从外到内)、目标阶段(事件目标)、冒泡阶段(从内到外)
- 第三个参数默认为
false。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件机制演示</title>
<style>
/* 三个嵌套的盒子样式 */
.box1{ width:500px; height:500px; background:red; }
.box2{ width:300px; height:300px; background:green; }
.box3{ width:100px; height:100px; background:blue; }
</style>
</head>
<body>
<!-- 嵌套的三层盒子结构 -->
<div class="box1">
<div class="box2">
<div class="box3"></div>
</div>
</div>
<script>
// 获取三个盒子元素
const box1 = document.querySelector('.box1');
const box2 = document.querySelector('.box2');
const box3 = document.querySelector('.box3');
// 事件监听设置:
// 1. box1在捕获阶段触发
box1.addEventListener('click', () => {
console.log('box1 red'); // 捕获阶段触发
}, true);
// 2. box2在冒泡阶段触发
box2.addEventListener('click', () => {
console.log('box2 green'); // 冒泡阶段触发
}, false);
// 3. box3在冒泡阶段触发
box3.addEventListener('click', () => {
console.log('box3 blue'); // 目标阶段触发
}, false);
</script>
</body>
</html>
所以当点击最内层的蓝色盒子(box3)时:
box1 red(捕获阶段)
box3 blue(目标阶段)
box2 green(冒泡阶段)
2. 事件委托
事件委托就是把事件监听器加到父元素上,利用事件冒泡的原理,来处理多个子元素的事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
// 给父元素ul添加一个监听器
let ul = document.querySelector('ul');
ul.addEventListener('click', function(e) {
// e.target就是被点击的子元素(li)
console.log(e.target.innerText);
});
</script>
</body>
</html>
1. 减少内存占用
// 传统方式(每个子元素绑定事件)
lis.forEach(li => li.addEventListener('click', handler));
// 事件委托(父元素统一处理)
ul.addEventListener('click', handler);
优势:当子元素数量多(如 1000 个 li)时,内存占用从 1000 个监听器减少到 1 个。
2. 自动支持动态内容
想象餐厅服务员(父元素)负责上菜,无论厨房新增多少道菜(动态子元素),服务员都能自动处理,无需重新培训每个厨师(绑定事件)。
// 新增动态元素后自动生效
ul.innerHTML += '<li>6</li>'; // 传统方式需要重新绑定,事件委托无需操作
优势:动态添加的子元素(如通过 AJAX 加载的数据)无需重新绑定事件。
3. 代码简洁统一
4. 性能优化
3. 跨域 存储 缓存
1. 跨域
详细可看:跨域问题全解析:七种方法轻松拿捏跨域
是因为浏览器有个同源策略(协议域名端口号),从一个域名的网页去请求另一个域名的资源就会受限。
跨域方案:
| 方案 | 核心原理 | 优点 | 缺点 | 典型场景 | 复杂度 | 兼容性 | 安全性 |
|---|---|---|---|---|---|---|---|
| JSONP | script 标签跨域加载 | 兼容性好,简单易用 | 仅 GET,XSS 风险 | 简单数据请求 | 低 | 高 | 低 |
| CORS | 服务端响应头控制 | 支持全方法,标准方案 | 需服务端配置,预请求 | 前后端分离项目 | 中 | 现代 | 高 |
| 反向代理 | 中间服务器转发请求 | 前端透明,支持复杂请求 | 需要额外服务器 | 企业级项目 | 高 | 高 | 高 |
| WebSocket | 独立协议长连接 | 实时双向通信 | 协议不同,需单独实现 | 聊天 / 推送 | 中 | 现代 | 高 |
| postMessage | 窗口间通信 API | 安全可控 | 仅限窗口通信 | iframe 交互 | 中 | 现代 | 高 |
| Node 代理 | 开发环境中间件转发 | 开发调试方便 | 生产环境不适用 | 前端开发阶段 | 中 | 现代 | 中 |
| document.domain | 主域名设置 | 历史方案 | 受浏览器限制,不安全 | 已废弃 | 低 | 差 | 低 |
常见的解决方案有:CORS 适用于需要支持多种 HTTP 方法(如 GET、POST、PUT 等)的现代 Web 应用,nginx 反向代理适用于前后端分离的项目,可以在服务器层面统一处理跨域问题。
2. 存储
| 存储类型 | 容量 | 生命周期 | 作用域 | 核心特点 | 典型场景 |
|---|---|---|---|---|---|
| Cookie | 约 4KB | 可设置过期时间(默认会话级) | 同源 + 指定路径 | 自动随 HTTP 请求发送,支持 Secure/HttpOnly/SameSite | 用户会话管理、跟踪行为 |
| LocalStorage | 5-10MB | 永久(需手动清除) | 同源窗口共享 | 同步 API,数据不参与 HTTP 通信 | 用户偏好、离线缓存 |
| SessionStorage | 5-10MB | 窗口关闭即清除 | 当前标签页 / 窗口 | 同步 API,新开窗口不共享 | 临时表单、单页应用会话状态 |
| IndexedDB | 无固定上限 | 永久 | 同源共享 | 异步 API,支持复杂查询和事务,存储二进制数据 | 大量数据存储(离线应用、媒体缓存) |
3. 缓存
| 缓存类型 | 控制方式 | 是否与服务器交互 | 优先级 | 典型应用 |
|---|---|---|---|---|
| 强缓存 | Cache-Control/Expires | 否 | 高 | 长期不变的静态资源 |
| 协商缓存 | ETag/Last-Modified | 是(验证) | 中 | 可能更新的动态资源 |
| Service Worker | 自定义脚本 | 可拦截 / 控制 | 灵活 | 离线应用、复杂缓存策略 |
4. 渲染过程
1. 解析 HTML 得到 DOM 树:
浏览器拿到 HTML 文档后,就开始一行一行解析。碰到标签,就创建对应的 DOM 节点对象,最后搭建成一棵 DOM 树。
2. 解析 CSS 得到 CSS 规则树:
与此同时,浏览器解析 CSS 样式表,把 CSS 规则变成它能懂的内部形式,形成 CSS 规则树。像div { color: red; },就会生成一条针对div元素的样式规则,里面有颜色属性color和值red,好多这样的规则组成 CSS 规则树。
3. 将 DOM 树和 CSS 规则树合并成 Render 树(只包含可见的节点) :
把 DOM 树和 CSS 规则树结合起来,按照 CSS 规则给 DOM 树里每个节点算最终样式,生成 Render 树。像设置了display: none的元素,就不会出现在 Render 树里,因为它看不见。
4. 根据 Render 树计算每个节点的位置 (回流) :
计算 Render 树里每个节点在页面上的几何位置,像元素的宽度、高度、位置这些。要是页面布局变了,比如说元素的width、height、margin、padding这些几何属性改了,就会触发回流。回流这个操作挺耗性能的,因为得重新算页面上所有元素的位置和大小。
5. 将每个节点绘制到屏幕上 (重绘) :
等每个节点的样式和位置都确定好了,浏览器就按照顺序把这些节点画到屏幕上,这就是最终看到的页面。要是元素的外观属性,像color、background-color、border-style这些变了,就会触发重绘。重绘相对回流来说,开销小一点,但也会影响性能。
1. 回流重绘
回流:当元素几何属性(如位置、尺寸、边距、字体大小等)改变,或触发布局变动时,浏览器需重新计算元素布局,更新渲染树结构,此过程为回流。例如:通过 JavaScript 修改元素 width 属性,或调整窗口大小,均会触发回流,其计算成本高,对页面性能影响较大。
重绘:若元素外观属性变化(如颜色、背景、阴影、透明度等),但不影响布局,浏览器重新绘制元素外观的过程即重绘。如修改 color 或 background-color,仅触发重绘,开销相对回流较小,但大量重绘仍会影响性能。
2. 浏览器的优化机制:队列处理
浏览器通过队列机制优化回流重绘:
- 批量处理:将多次回流重绘操作存入队列,待操作数量达标或时间阈值到达,统一执行,降低渲染损耗。
- 强制刷新:读取布局属性(如
offsetTop、clientWidth、scrollHeight等)或调用getComputedStyle()时,会强制刷新队列,立即执行所有待处理操作,可能影响性能。
3. 减少回流重绘的优化策略
1. 批量样式修改
反例:逐行修改样式,频繁触发回流重绘。
element.style.color = 'red';
element.style.fontSize = '16px';
优化:通过 CSS 类批量应用样式。
.optimize-style {
color: red;
font-size: 16px;
}
element.className = 'optimize-style';
2. 脱离文档流操作
对需大量回流的元素,先脱离文档流,完成操作后再回归。
应用场景:复杂动画、大规模 DOM 修改。
示例:
// 脱离文档流
element.style.position = 'absolute';
// 执行复杂操作
// ...
// 恢复原布局
element.style.position = 'static';
3. 利用文档碎片(DocumentFragment)
文档碎片是内存中的轻量级 DOM 容器,操作时不触发页面回流。
示例:批量添加列表项。
const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById('myList').appendChild(fragment);
4. 克隆 DOM 操作
克隆元素,在副本上操作,最后替换原元素,减少对真实 DOM 的直接操作。
示例:修改复杂表格。
const originalTable = document.getElementById('table');
const cloneTable = originalTable.cloneNode(true);
// 在克隆表格上操作:添加行、修改数据等
originalTable.parentNode.replaceChild(cloneTable, originalTable);
5. 读写分离
避免混合读写布局属性,先集中写操作,再统一读操作。
反例:读写混合,强制刷新队列。
element.style.width = '200px';
const height = element.offsetHeight;
element.style.height = '300px';
优化:先写后读。
element.style.width = '200px';
element.style.height = '300px';
const width = element.offsetWidth;
const height = element.offsetHeight;
6. 硬件加速
利用 CSS 属性触发 GPU 加速,减少回流重绘。
示例:
.accelerate-element {
transform: translateZ(0); /* 启用 GPU 加速 */
will-change: transform; /* 提示浏览器优化 */
}
四、设计模式 - 发布订阅模式
发布订阅模式在 JavaScript 开发里用得挺多,它定义了一种一对多的依赖关系,就是好多订阅者对象同时监听一个发布者对象。发布者对象状态一变,就通知所有订阅者对象,让它们自动更新。
这种模式的核心优势在于:
- 解耦性:发布者和订阅者无需直接关联,只需通过事件名称进行交互
- 扩展性:新增订阅者或修改发布者逻辑时无需改动另一方代码
- 异步通信:事件触发与处理可以异步执行,提升系统响应能力
1. 核心组件解析
1. 发布者(Publisher)
- 负责触发特定事件
- 不关心谁在监听这些事件
- 示例:DOM 元素的事件触发机制
2. 订阅者(Subscriber)
- 注册监听特定事件
- 接收事件通知并执行回调
- 示例:JavaScript 中的事件监听函数
3. 事件中心(Event Bus)
- 维护事件与订阅者的映射关系
- 提供订阅、取消订阅和触发事件的接口
- 示例:Vue.js 的
EventEmitter或 React 的自定义事件系统
2. 基础实现与代码解析
class EventEmitter {
constructor() {
this.events = {}; // 存储事件与回调的映射
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
// 触发事件
emit(eventName, ...args) {
this.events[eventName]?.forEach(callback => {
callback(...args);
});
}
// 取消订阅
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName]
.filter(cb => cb !== callback);
}
// 单次订阅
once(eventName, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
}
关键特性说明:
1. 多事件管理:通过对象存储不同事件的回调列表
2. 参数传递:支持事件触发时传递任意参数
3. 取消订阅:提供 off 方法避免内存泄漏
4. 单次触发:once 方法实现只执行一次的订阅
3. 典型应用场景
1. 前端框架中的组件通信
- React:通过 Context API + 自定义事件实现跨层级通信
- Vue:早期版本的
Event Bus模式 - 示例代码:
// 父子组件通信
const emitter = new EventEmitter();
// 子组件触发事件
emitter.emit('update', { data: 'new value' });
// 父组件监听事件
emitter.on('update', (payload) => {
console.log('Received:', payload.data);
});
2. 异步编程中的事件驱动
- 网络请求完成通知
- 文件上传进度监听
- 实时数据更新推送
3. 游戏开发中的事件系统
- 碰撞检测事件
- 角色状态变更通知
- 用户输入响应
4. 与观察者模式的区别
| 特性 | 发布订阅模式 | 观察者模式 |
|---|---|---|
| 解耦程度 | 完全解耦(通过事件中心) | 部分解耦(主题与观察者直接关联) |
| 事件触发方式 | 主动触发 emit 方法 | 状态变化自动通知 |
| 应用场景 | 跨系统通信 | 单一系统内的状态同步 |
| 典型实现 | 消息队列(RabbitMQ) | Vue 的响应式系统 |