Day18_JavaScript高级核心:原型链、继承与事件循环机制深度解析(下)
数据传递机制
4.2 实战应用场景
完整的Worker示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Worker多线程计算</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input {
padding: 10px;
font-size: 16px;
width: 120px;
}
button {
padding: 10px 20px;
font-size: 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #45a049;
}
#res {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
font-size: 18px;
}
</style>
</head>
<body>
<h1>Web Worker 计算器</h1>
<div class="input-group">
<input type="number" id="input01" placeholder="第一个数">
<input type="number" id="input02" placeholder="第二个数">
<button id="btn">计算</button>
</div>
<div id="res"></div>
<script>
// 【代码注释】获取DOM元素
var input01 = document.querySelector('#input01');
var input02 = document.querySelector('#input02');
var btn = document.querySelector('#btn');
var resBox = document.querySelector('#res');
// 【代码注释】创建Worker线程
// Worker线程会加载并执行child.js文件
var worker = new Worker('./child.js');
// 【代码注释】点击按钮时向Worker发送数据
btn.onclick = function() {
// 【代码注释】获取输入值并转换为数字
var nums = [+input01.value, +input02.value];
// 【代码注释】数据验证
if (isNaN(nums[0]) || isNaN(nums[1])) {
resBox.innerHTML = '<span style="color: red;">请输入有效数字</span>';
return;
}
// 【代码注释】向Worker线程发送数据
worker.postMessage(nums);
resBox.innerHTML = '计算中...';
};
// 【代码注释】监听Worker返回的结果
worker.onmessage = function(event) {
resBox.innerHTML = '<span style="color: green;">计算结果:' + event.data + '</span>';
};
// 【代码注释】错误处理
worker.onerror = function(error) {
resBox.innerHTML = '<span style="color: red;">错误:' + error.message + '</span>';
console.error('Worker错误:', error);
};
</script>
</body>
</html>
【代码注释】
主线程职责
new Worker('./child.js'):另开线程加载脚本;主线程只负责 UI 与postMessage/onmessage通信。btn.onclick:+input01.value转数字;postMessage(nums)通过结构化克隆传数组(非引用共享)。onmessage:event.data为 Worker 回传结果;onerror捕获脚本加载或运行错误。
注意点
- Worker 文件需与页面同源或通过 blob URL;路径错误会触发
onerror。 - 主线程
postMessage后立刻显示「计算中…」,UI 不阻塞——与 child.js 内百万次Math.sqrt对比。
Worker脚本文件(child.js):
// 【代码注释】Worker内部代码
// 监听主线程发过来的数据
onmessage = function(event) {
// 【代码注释】event.data包含主线程发送的数据
var data = event.data; // [num1, num2]
// 【代码注释】执行计算
var res = data[0] + data[1];
// 【代码注释】模拟耗时操作
for (var i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
// 【代码注释】将结果发送回主线程
postMessage(res);
};
// 【代码注释】也可以使用addEventListener的方式
/*
self.addEventListener('message', function(event) {
var result = event.data[0] + event.data[1];
self.postMessage(result);
});
*/
【代码注释】
Worker 线程内
onmessage:接收主线程event.data(此处为[num1, num2])。- 耗时
for循环在子线程执行,不占用主线程 Event Loop,页面仍可响应点击。 postMessage(res):将结果传回主线程,触发主线程worker.onmessage。
通信模型
- 双向异步消息,无共享内存(
SharedArrayBuffer需额外安全头,本文未涉及)。 - Worker 内全局为
self,无window/document。
Worker经典应用场景
1. 大数据处理
// 【代码注释】主线程
const dataWorker = new Worker('dataProcessor.js');
dataWorker.onmessage = function(e) {
const result = e.data;
console.log('处理完成,结果行数:', result.length);
};
// 【代码注释】发送大型数组
dataWorker.postMessage({
type: 'process',
data: largeArray // 假设是百万级数据
});
// 【代码注释】Worker内部(dataProcessor.js)
self.onmessage = function(e) {
if (e.data.type === 'process') {
const processed = e.data.data.map(item => {
// 复杂的数据处理
return transformData(item);
});
self.postMessage(processed);
}
};
2. 图像处理
// 【代码注释】图像处理Worker
self.onmessage = function(e) {
const imageData = e.data;
const data = imageData.data;
// 【代码注释】像素级图像处理
for (let i = 0; i < data.length; i += 4) {
// 灰度处理
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
self.postMessage(imageData);
};
3. 密码学计算
// 【代码注释】加密/哈希计算Worker
self.onmessage = function(e) {
const data = e.data;
// 【代码注释】执行CPU密集的加密操作
const hash = performHeavyHashing(data);
self.postMessage({
type: 'hash',
result: hash
});
};
Worker使用限制
/*
【Web Worker限制】
1. 无法访问DOM(document、window、parent)
2. 无法访问某些全局对象(localStorage)
3. 只能通过postMessage通信
4. 同源策略限制
5. Worker内加载的脚本也受同源限制
【可用对象】
- navigator:获取浏览器信息
- location:只读访问
- XMLHttpRequest/fetch:发起网络请求
- setTimeout/setInterval:定时器
- IndexedDB:本地数据库
- importScripts:加载其他脚本
*/
五、核心知识点总结
5.1 原型链总结
5.2 事件循环机制总结
5.3 最佳实践建议
原型链使用建议
// 【建议1】不要随意修改内置对象原型
// ❌ 不推荐
Array.prototype.myMethod = function() {};
// ✅ 推荐:使用类或模块封装
class MyArray extends Array {
myMethod() {}
}
// 【建议2】正确使用Object.create
const obj = Object.create(null); // 无原型的纯净对象
console.log(obj.toString); // undefined
// 【建议3】使用hasOwnProperty检查自有属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 只处理对象自身的属性
}
}
异步编程建议
// 【建议1】优先使用Promise/async-await
// ✅ 推荐
async function fetchData() {
try {
const data = await fetch('/api/data');
return await data.json();
} catch (error) {
console.error('Error:', error);
}
}
// 【建议2】避免回调地狱
// ❌ 回调地狱
asyncFunc1(function() {
asyncFunc2(function() {
asyncFunc3(function() {
// ...
});
});
});
// ✅ Promise链式
asyncFunc1()
.then(() => asyncFunc2())
.then(() => asyncFunc3())
.catch(console.error);
// ✅ async-await更优雅
async function execute() {
await asyncFunc1();
await asyncFunc2();
await asyncFunc3();
}
5.4 知识点对比表
| 特性 | 原型链 | 闭包 | 事件循环 | Web Worker |
|---|---|---|---|---|
| 核心作用 | 实现继承和方法共享 | 保护变量和状态管理 | 实现异步编程 | 实现多线程 |
| 使用场景 | 对象继承、方法扩展 | 模块化、数据缓存 | 网络请求、定时器 | 复杂计算、数据处理 |
| 注意事项 | constructor修正 | 内存泄漏风险 | 执行顺序理解 | DOM不可访问 |
六、经典工程场景实战
本章将前五章的理论知识映射到真实项目中的高频工程场景,每个案例均完整可运行,并给出真实网站的对应应用。
6.1 闭包实战:防抖、节流、记忆化
防抖(Debounce)和节流(Throttle)是前端性能优化中最高频使用的闭包模式,几乎每个生产项目都会用到。
防抖(Debounce)——搜索框输入联想
应用场景: 搜索框输入、窗口 resize、表单校验——用户停止操作后才触发,避免频繁请求。
/**
* 防抖:连续触发时,只执行最后一次。
* 原理:利用闭包保存 timer,每次触发先清除旧 timer,再设新 timer。
*/
function debounce(fn, delay) {
let timer = null; // 闭包变量:持久化 timer ID
return function(...args) {
// 每次触发先清除上一次的定时器
clearTimeout(timer);
// 重新设置定时器,delay 毫秒后执行
timer = setTimeout(() => {
fn.apply(this, args); // 保持 this 指向调用者
}, delay);
};
}
// ── 使用示例:搜索框输入联想 ──
const searchInput = document.querySelector('#search');
const handleSearch = debounce(function(e) {
const keyword = e.target.value.trim();
if (!keyword) return;
console.log('发起搜索请求:', keyword);
// 实际项目中:fetch(`/api/search?q=${keyword}`)
}, 300);
searchInput.addEventListener('input', handleSearch);
// ── 带"立即执行"选项的增强版 ──
function debounceAdvanced(fn, delay, immediate = false) {
let timer = null;
return function(...args) {
const callNow = immediate && !timer; // 首次立即执行
clearTimeout(timer);
timer = setTimeout(() => {
timer = null; // 重置,允许下一轮立即执行
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
}
// 立即执行版本:第一次触发立刻执行,之后静默 delay 毫秒
const submitBtn = document.querySelector('#submit');
const handleSubmit = debounceAdvanced(function() {
console.log('提交表单(防重复提交)');
}, 2000, true);
submitBtn.addEventListener('click', handleSubmit);
【代码注释】
核心逻辑
debounce每次触发先clearTimeout(timer),再setTimeout延迟delayms 执行fn——连续输入时只有最后一次停顿满delay才触发。timer存在闭包中,返回的包装函数多次调用共享同一timer变量,实现「取消上一次、预约下一次」。debounceAdvanced中immediate && !timer:首次触发且当前无等待中的定时器时立即执行;timer = null在定时器回调里重置,允许下一轮再次 immediate。
关键 API
clearTimeout(timer)/setTimeout:防抖的核心调度手段。fn.apply(this, args):保持调用方this(如 input 的addEventListener回调里this为元素)。
注意点
- 包装函数内若用箭头函数
() => fn.apply(...)一般无妨,但若fn依赖调用时的this,外层必须用普通函数返回。 - 搜索建议
delay常 300ms;resize 常 150–300ms;提交防重复 1000–2000ms。
实战场景
- 百度搜索联想、Element Plus
el-input远程搜索、窗口 resize 后重算布局,均为防抖典型。
节流(Throttle)——滚动加载与 resize
应用场景: 页面滚动、鼠标移动、射击游戏按键——以固定频率执行,避免过于密集的调用。
/**
* 节流(时间戳版):每隔 interval 毫秒最多执行一次。
* 特点:第一次触发立即执行,最后一次可能被忽略。
*/
function throttleTimestamp(fn, interval) {
let lastTime = 0; // 闭包变量:上次执行的时间戳
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
/**
* 节流(定时器版):第一次触发有延迟,但最后一次一定会执行。
*/
function throttleTimer(fn, interval) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, interval);
}
};
}
/**
* 完整版节流:结合两种模式——第一次立即执行,最后一次也保证执行。
*/
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
// 距离上次已超过 interval,立即执行
if (timer) { clearTimeout(timer); timer = null; }
lastTime = now;
fn.apply(this, args);
} else {
// 剩余时间内再次触发,等待剩余时间后执行(保证最后一次)
clearTimeout(timer);
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
// ── 使用示例:滚动加载更多 ──
const handleScroll = throttle(function() {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
const isBottom = scrollTop + clientHeight >= scrollHeight - 100;
if (isBottom) {
console.log('触底,加载更多数据...');
// loadMoreData();
}
}, 200);
window.addEventListener('scroll', handleScroll);
// ── 使用示例:拖拽位置更新 ──
const handleMouseMove = throttle(function(e) {
console.log(`鼠标位置: ${e.clientX}, ${e.clientY}`);
// 每 16ms(约 60fps)更新一次,避免卡顿
}, 16);
document.addEventListener('mousemove', handleMouseMove);
【代码注释】
核心逻辑
- 时间戳版:
now - lastTime >= interval才执行,首次立即响应,快速滚动中间可能跳过部分帧,最后一次可能不执行。 - 定时器版:
if (!timer)只设一次定时器,首次有 delay,但在持续触发下会周期性执行,最后一次一般会落到定时器里。 - 完整版:
remaining <= 0时立即执行;否则用setTimeout(..., remaining)保证冷却结束后再执行一次,兼顾首尾。
关键参数
interval = 16≈ 1000/60,与显示器 60Hz 刷新对齐,避免mousemove每秒触发上百次导致卡顿。scroll监听建议{ passive: true },因通常不需preventDefault。
注意点
- 防抖 vs 节流:防抖「停下来才做」;节流「每隔一段时间做一次」。滚轮缩放地图用节流,搜索输入用防抖。
- Lodash 的
_.throttle默认带 leading/trailing 选项,与完整版思路一致。
实战场景
- 淘宝/京东列表触底加载、高德地图拖拽、射击游戏连发限制、Canvas 鼠标轨迹采样。
真实网站场景: 淘宝商品列表页的"滚动触底加载更多"、百度地图拖拽时的坐标计算,均使用节流控制回调频率。
记忆化(Memoization)——斐波那契与接口缓存
应用场景: 纯函数的结果缓存,避免相同参数重复计算(斐波那契、动态规划、接口数据缓存)。
/**
* 通用记忆化工厂函数
* 利用闭包保存 cache Map,相同参数直接返回缓存结果。
*/
function memoize(fn, resolver) {
const cache = new Map(); // 闭包变量:结果缓存
const memoized = function(...args) {
// resolver 可自定义缓存 key(默认用第一个参数)
const key = resolver ? resolver(...args) : JSON.stringify(args);
if (cache.has(key)) {
console.log(`[cache hit] key=${key}`);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
// 暴露缓存管理接口
memoized.cache = cache;
memoized.clear = () => cache.clear();
return memoized;
}
// ── 应用1:斐波那契数列(指数 → 线性复杂度)──
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2); // 递归调用的是 memoize 包装后的版本
});
console.time('fib(40)');
console.log(fibonacci(40)); // 102334155
console.timeEnd('fib(40)'); // 极快,无缓存版需要数秒
// ── 应用2:接口请求缓存(避免重复请求同一资源)──
const fetchUser = memoize(async function(userId) {
console.log(`发起请求: /api/users/${userId}`);
const response = await fetch(`/api/users/${userId}`);
return response.json();
}, (userId) => `user_${userId}`); // 自定义 key
// 两次调用相同 userId,只发一次网络请求
fetchUser(123).then(user => console.log('第一次:', user));
fetchUser(123).then(user => console.log('第二次(缓存):', user));
fetchUser(456).then(user => console.log('不同 ID:', user));
// ── 应用3:带 TTL(过期时间)的缓存 ──
function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
// 检查缓存是否存在且未过期
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, { value: result, timestamp: Date.now() });
return result;
};
}
// 接口数据缓存 60 秒
const getProductList = memoizeWithTTL(async function(category) {
const res = await fetch(`/api/products?category=${category}`);
return res.json();
}, 60000);
【代码注释】
核心逻辑
memoize返回的memoized闭包持有cache(Map),外层memoize执行结束后cache仍存活——闭包延长变量生命周期的典型。- 命中:
cache.has(key)直接get;未命中:fn.apply计算后set,resolver可自定义 key(默认JSON.stringify(args))。 memoized.cache/clear:暴露缓存管理,便于调试或登录后清空用户缓存。
斐波那契与接口缓存
- 递归必须调用包装后的
fibonacci(n-1),否则子问题不入缓存,复杂度仍为指数级。 fetchUser用(userId) => \user_${userId}`作 key,相同 ID 的并发两次then` 只触发一次真实请求(需注意 Promise 未完成的竞态,生产可加 pending Map)。
TTL 版本
- 缓存项存
{ value, timestamp },过期后重新请求,适合价格、库存等会变的接口。
注意点
JSON.stringify作 key 时,对象属性顺序敏感;复杂参数建议自定义resolver。- 缓存无限增长会导致内存问题,需
clear或 LRU 策略。
实战场景
- React
useMemo/useCallback、Vuecomputed、接口层 dedupe、lodashmemoize。
真实网站场景: React 的 useMemo / useCallback、Vue 的计算属性(computed),本质上都是框架层面的记忆化机制。
6.2 闭包实战:模块化与私有状态
在 ES6 模块普及之前,闭包是 JavaScript 实现模块化和私有变量的唯一方式;即使现在,理解这个模式对阅读老代码和理解 IIFE 不可或缺。
购物车模块
/**
* 购物车模块:用 IIFE + 闭包实现完整的私有状态管理
* 外部无法直接访问 _items、_total,只能通过暴露的 API 操作
*/
const ShoppingCart = (function() {
// ── 私有状态(外部无法访问)──
let _items = [];
let _discount = 0;
// ── 私有函数 ──
function _calcTotal() {
const subtotal = _items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
return subtotal * (1 - _discount);
}
function _findItem(id) {
return _items.find(item => item.id === id);
}
// ── 公开 API ──
return {
// 添加商品
addItem(product, quantity = 1) {
const existing = _findItem(product.id);
if (existing) {
existing.quantity += quantity;
} else {
_items.push({ ...product, quantity });
}
console.log(`已添加:${product.name} ×${quantity}`);
return this; // 支持链式调用
},
// 移除商品
removeItem(id) {
const index = _items.findIndex(item => item.id === id);
if (index > -1) {
const [removed] = _items.splice(index, 1);
console.log(`已移除:${removed.name}`);
}
return this;
},
// 修改数量
updateQuantity(id, quantity) {
const item = _findItem(id);
if (item) {
if (quantity <= 0) return this.removeItem(id);
item.quantity = quantity;
}
return this;
},
// 设置折扣
setDiscount(rate) {
if (rate < 0 || rate > 1) throw new Error('折扣率必须在 0~1 之间');
_discount = rate;
console.log(`已设置折扣:${(rate * 100).toFixed(0)}% off`);
return this;
},
// 获取总价
getTotal() {
return _calcTotal().toFixed(2);
},
// 获取商品列表(返回副本,防止外部修改内部状态)
getItems() {
return _items.map(item => ({ ...item }));
},
// 清空购物车
clear() {
_items = [];
_discount = 0;
console.log('购物车已清空');
return this;
},
// 结算信息
checkout() {
if (_items.length === 0) {
console.log('购物车为空!');
return null;
}
const order = {
items: this.getItems(),
discount: _discount,
total: this.getTotal(),
createdAt: new Date().toISOString()
};
this.clear();
return order;
}
};
})();
// ── 使用示例 ──
ShoppingCart
.addItem({ id: 1, name: '机械键盘', price: 359 }, 1)
.addItem({ id: 2, name: '无线鼠标', price: 129 }, 2)
.addItem({ id: 1, name: '机械键盘', price: 359 }, 1) // 已有商品,数量叠加
.setDiscount(0.1); // 九折优惠
console.log('商品列表:', ShoppingCart.getItems());
console.log('应付金额:¥', ShoppingCart.getTotal()); // (359×2 + 129×2) × 0.9
// 无法直接访问私有变量
console.log(ShoppingCart._items); // undefined
console.log(ShoppingCart._discount); // undefined
const order = ShoppingCart.checkout();
console.log('订单:', order);
【代码注释】
核心逻辑
- IIFE
(function(){ ... return { ... }; })()执行一次,_items、_discount、_calcTotal对外不可见,仅暴露addItem、checkout等 API。 addItem合并同id商品数量;setDiscount校验 0~1;checkout用getItems()快照后clear(),生成订单对象。
封装细节
return this:链式ShoppingCart.addItem(...).setDiscount(0.1)。getItems()返回map浅拷贝新对象,避免外部改_items内部引用;若需防改嵌套字段可再深拷贝。
注意点
ShoppingCart._items为undefined——私有变量不在返回对象上,不是「下划线约定公开」。- ES Module 的
let+ 导出函数可替代 IIFE;读老代码、UMD 包时 IIFE 仍常见。
实战场景
- 早期模块模式、Redux/Pinia 的「单一 store + 不可直接改 state」、SDK 只暴露有限方法。
真实网站场景: 早期 jQuery 插件、RequireJS 模块均基于此模式;现代框架的 Vuex/Pinia store 在概念上与此相同——私有 state,通过 actions/mutations 操作。
6.3 原型链实战:观察者模式(EventEmitter)
观察者模式(发布-订阅)是前端框架(Vue 响应式、Node.js EventEmitter)的核心设计,用原型链实现是学习该模式的最佳路径。
/**
* EventEmitter:基于原型链的事件发布-订阅系统
* 这是 Node.js events 模块的简化版实现
*/
function EventEmitter() {
// 实例属性:事件监听器存储(不放在原型上,每个实例独立)
this._events = Object.create(null);
this._maxListeners = 10;
}
// ── 在原型上定义共享方法 ──
/** 订阅事件 */
EventEmitter.prototype.on = function(event, listener) {
if (!this._events[event]) {
this._events[event] = [];
}
// 检查是否超过最大监听器数量(内存泄漏预警)
if (this._events[event].length >= this._maxListeners) {
console.warn(`警告:事件 "${event}" 的监听器数量超过 ${this._maxListeners}`);
}
this._events[event].push(listener);
return this; // 链式调用
};
/** 订阅一次性事件(触发后自动取消) */
EventEmitter.prototype.once = function(event, listener) {
// 用包装函数确保只执行一次
const wrapper = (...args) => {
listener.apply(this, args);
this.off(event, wrapper); // 执行后立即移除
};
wrapper._original = listener; // 保存原始函数引用,供 off 使用
return this.on(event, wrapper);
};
/** 取消订阅 */
EventEmitter.prototype.off = function(event, listener) {
if (!this._events[event]) return this;
this._events[event] = this._events[event].filter(fn => {
// 同时检查 once 包装函数的原始引用
return fn !== listener && fn._original !== listener;
});
return this;
};
/** 发布事件 */
EventEmitter.prototype.emit = function(event, ...args) {
if (!this._events[event] || this._events[event].length === 0) {
return false;
}
// 复制一份,防止回调中的 off 操作影响遍历
const listeners = [...this._events[event]];
listeners.forEach(fn => fn.apply(this, args));
return true;
};
/** 获取事件的所有监听器 */
EventEmitter.prototype.listeners = function(event) {
return this._events[event] ? [...this._events[event]] : [];
};
/** 移除所有监听器 */
EventEmitter.prototype.removeAllListeners = function(event) {
if (event) {
delete this._events[event];
} else {
this._events = Object.create(null);
}
return this;
};
// ── 继承 EventEmitter ──
/**
* 用户模块:继承 EventEmitter,拥有事件能力
*/
function UserStore() {
EventEmitter.call(this); // 借用构造函数:初始化 _events
this.users = [];
this.loading = false;
}
// 原型链继承
UserStore.prototype = Object.create(EventEmitter.prototype);
UserStore.prototype.constructor = UserStore;
UserStore.prototype.fetchUsers = async function() {
this.loading = true;
this.emit('loading', true);
try {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 500));
this.users = [
{ id: 1, name: '张三', role: 'admin' },
{ id: 2, name: '李四', role: 'user' },
{ id: 3, name: '王五', role: 'user' },
];
this.emit('change', this.users); // 数据变化通知
this.emit('success', this.users);
} catch (err) {
this.emit('error', err);
} finally {
this.loading = false;
this.emit('loading', false);
}
};
UserStore.prototype.addUser = function(user) {
this.users.push(user);
this.emit('change', this.users);
this.emit('add', user);
return this;
};
// ── 使用示例 ──
const userStore = new UserStore();
// 订阅事件
userStore
.on('loading', isLoading => {
console.log(isLoading ? '加载中...' : '加载完毕');
// 实际项目中:更新 loading 状态 spinner
})
.on('change', users => {
console.log('用户列表更新,共', users.length, '条');
// 实际项目中:重新渲染列表
})
.on('error', err => {
console.error('请求失败:', err.message);
})
.once('success', users => {
console.log('首次加载成功,初始化完成'); // 只触发一次
});
userStore.fetchUsers();
// 验证继承关系
console.log(userStore instanceof UserStore); // true
console.log(userStore instanceof EventEmitter); // true
console.log(userStore.__proto__ === UserStore.prototype); // true
console.log(userStore.__proto__.__proto__ === EventEmitter.prototype); // true
【代码注释】
核心逻辑
EventEmitter构造函数:每个实例独立的this._events字典,event名 → 监听器数组。on:向数组push监听器;emit时slice复制数组再遍历,防止回调里off导致遍历时删元素。once:包装wrapper,首次执行后off(event, wrapper);wrapper._original供off匹配原始函数。UserStore:EventEmitter.call(this)初始化_events;Object.create(EventEmitter.prototype)继承原型方法;fetchUsers内emit('change')通知订阅方。
关键 API
emit(event, ...args):listener.apply(this, args)使监听器内this指向 emitter 实例。Object.create(null):无__proto__,避免_events['toString']等键与原型链冲突。
注意点
- 监听器过多未
off会内存泄漏;Node 对warning: possible EventEmitter memory leak即此问题。 - Vue 3 推荐
mitt/emitter;Vue 2 的$on/$emit与本文模型一致。
实战场景
- Node.js
events、Socket.io、Redux 中间件订阅、自定义组件库「数据变更通知」。
真实网站场景: Node.js 的 events 模块、Vue 2 的响应式系统(Dep/Watcher)、Socket.io 的事件系统,都是 EventEmitter 模式的工业级实现。
6.4 原型链实战:策略模式与混入(Mixin)
策略模式——表单校验
策略模式将算法族封装为独立对象,通过原型链组织,运行时动态切换策略,彻底消除 if-else 分支。
/**
* 表单校验策略模式
* 每种校验规则是一个独立的策略对象
*/
// ── 策略对象集合 ──
const Validators = {
required(value, msg = '该字段不能为空') {
return value !== null && value !== undefined && String(value).trim() !== ''
? null
: msg;
},
minLength(value, min, msg) {
return String(value).length >= min
? null
: msg || `最少输入 ${min} 个字符`;
},
maxLength(value, max, msg) {
return String(value).length <= max
? null
: msg || `最多输入 ${max} 个字符`;
},
phone(value, msg = '手机号格式不正确') {
return /^1[3-9]\d{9}$/.test(String(value)) ? null : msg;
},
email(value, msg = '邮箱格式不正确') {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value)) ? null : msg;
},
number(value, msg = '请输入有效数字') {
return !isNaN(Number(value)) && value !== '' ? null : msg;
},
range(value, min, max, msg) {
const num = Number(value);
return num >= min && num <= max
? null
: msg || `数值必须在 ${min}~${max} 之间`;
},
pattern(value, regex, msg = '格式不正确') {
return regex.test(String(value)) ? null : msg;
}
};
// ── 校验器构造函数 ──
function FormValidator() {
this._rules = {}; // { fieldName: [ {strategy, args}, ... ] }
}
FormValidator.prototype.addRule = function(field, strategy, ...args) {
if (!this._rules[field]) this._rules[field] = [];
this._rules[field].push({ strategy, args });
return this; // 链式
};
FormValidator.prototype.validate = function(formData) {
const errors = {};
for (const field in this._rules) {
for (const rule of this._rules[field]) {
const { strategy, args } = rule;
const validator = Validators[strategy];
if (!validator) {
console.warn(`未知校验策略:${strategy}`);
continue;
}
const error = validator(formData[field], ...args);
if (error) {
errors[field] = error;
break; // 同一字段遇到错误停止后续校验
}
}
}
return {
valid: Object.keys(errors).length === 0,
errors
};
};
// ── 使用示例:注册表单 ──
const registerValidator = new FormValidator();
registerValidator
.addRule('username', 'required')
.addRule('username', 'minLength', 3)
.addRule('username', 'maxLength', 20)
.addRule('phone', 'required')
.addRule('phone', 'phone')
.addRule('email', 'email')
.addRule('age', 'number')
.addRule('age', 'range', 18, 120, '年龄必须在 18~120 之间')
.addRule('password', 'required')
.addRule('password', 'minLength', 8, '密码至少 8 位');
const formData = {
username: 'ab', // 太短
phone: '138abc', // 格式错误
email: 'user@example.com',
age: 25,
password: '12345678'
};
const { valid, errors } = registerValidator.validate(formData);
console.log('是否通过:', valid); // false
console.log('错误信息:', errors);
// { username: '最少输入 3 个字符', phone: '手机号格式不正确' }
【代码注释】
核心逻辑
Validators为策略字典:required、phone等返回null(通过)或错误文案(失败),与FormValidator解耦。addRule(field, strategy, ...args)向_rules[field]推入规则队列;validate按字段遍历,调用Validators[strategy](value, ...args)。- 同一字段遇错
break,只保留第一条错误,避免一次展示满屏红字。
设计模式
- 策略模式:消除
if (type === 'phone')大分支,新增规则只加Validators.xxx(开闭原则)。 - 链式 API 与 Element Plus 表单规则、Yup/Joi 声明式校验同源。
注意点
strategy拼写错误会console.warn并跳过,生产应在校验前做规则注册表检查。- 异步校验(如查重用户名)需扩展为返回 Promise 的版本。
Mixin(混入)——横切关注点
Mixin 解决多继承问题:一个对象需要多个"能力",但 JavaScript 只支持单原型链。
/**
* Mixin 工厂:将多个能力对象的方法混入到目标构造函数的原型
*/
function mixins(TargetClass, ...mixinObjects) {
mixinObjects.forEach(mixin => {
Object.getOwnPropertyNames(mixin).forEach(key => {
if (key === 'constructor') return; // 跳过 constructor
// 使用 Object.defineProperty 保留属性描述符
if (!TargetClass.prototype[key]) {
Object.defineProperty(
TargetClass.prototype,
key,
Object.getOwnPropertyDescriptor(mixin, key)
);
}
});
});
return TargetClass;
}
// ── 能力 Mixin 对象 ──
const SerializableMixin = {
toJSON() {
return JSON.stringify(this);
},
fromJSON(json) {
return Object.assign(this, JSON.parse(json));
}
};
const ValidatableMixin = {
validate() {
const errors = [];
if (this._validateRules) {
this._validateRules.forEach(rule => {
const error = rule(this);
if (error) errors.push(error);
});
}
return errors;
},
addValidation(fn) {
if (!this._validateRules) this._validateRules = [];
this._validateRules.push(fn);
return this;
}
};
const LoggableMixin = {
log(message) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${this.constructor.name}] ${message}`);
return this;
},
warn(message) {
console.warn(`[${this.constructor.name}] ⚠ ${message}`);
return this;
}
};
// ── 将 Mixin 混入业务类 ──
function Product(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
// 混入三种能力
mixins(Product, SerializableMixin, ValidatableMixin, LoggableMixin);
// ── 使用示例 ──
const p = new Product(1, '机械键盘', 359);
// 序列化能力
console.log(p.toJSON()); // '{"id":1,"name":"机械键盘","price":359}'
// 校验能力
p.addValidation(self => self.price > 0 ? null : '价格必须大于 0')
.addValidation(self => self.name.length > 0 ? null : '名称不能为空');
console.log(p.validate()); // [](无错误)
// 日志能力
p.log('商品对象创建成功').warn('库存不足,请及时补货');
// 验证 Mixin 不影响原型链
console.log(p instanceof Product); // true(原型链未破坏)
【代码注释】
核心逻辑
mixins(TargetClass, ...mixinObjects):遍历每个 mixin 的自有属性名,用Object.defineProperty复制到TargetClass.prototype(保留 getter/setter 描述符)。- 跳过
constructor;若原型上已有同名方法则不覆盖(子类/目标类优先)。 Product混入序列化、校验、日志三种横切能力,实例p.toJSON()/p.validate()/p.log()均来自原型。
与继承对比
| 继承 | Mixin | |
|---|---|---|
| 关系 | is-a(子类是一种父类) | has-a / 能力叠加 |
instanceof |
沿原型链成立 | 不增加父类型 |
| 多父类 | JS 单链,需组合继承 | 可叠多个能力对象 |
注意点
- Vue 2
mixins合并选项有优先级与命名冲突问题;React 16+ 更推荐 Hooks 组合。 - 过多 Mixin 导致原型方法来源难追踪,需文档约定。
实战场景
- Vue 2 mixins、Lodash
_.mixin、工具类给 Model 加save/validate。
真实网站场景: Vue 2 的 mixins 选项、React 的 HOC(Higher-Order Component)、Lodash 的 _.mixin,本质上都是此模式的不同实现形式。
6.5 继承实战:组件基类封装
用 ES5 原型继承模拟一个简单的"UI 组件框架"基类,直观展示继承在实际项目中的价值。
/**
* 组件基类(Component Base Class)
* 封装所有组件共用的挂载、渲染、更新、销毁生命周期
*/
function Component(options) {
this.el = options.el || null; // 挂载元素
this.data = options.data || {}; // 组件数据
this._mounted = false;
this._destroyed = false;
this._listeners = {}; // 保存事件监听器(用于销毁时清理)
this.created && this.created(); // 触发生命周期:created
}
Component.prototype.mount = function(container) {
if (this._mounted) return this;
const el = typeof container === 'string'
? document.querySelector(container)
: container;
if (!el) throw new Error(`挂载目标不存在:${container}`);
this.el = el;
const vdom = this.render(); // 调用子类实现的 render
this.el.innerHTML = vdom;
this._mounted = true;
this._bindEvents && this._bindEvents(); // 绑定事件
this.mounted && this.mounted(); // 生命周期:mounted
return this;
};
Component.prototype.update = function(newData) {
if (this._destroyed) return;
Object.assign(this.data, newData); // 合并新数据
this.beforeUpdate && this.beforeUpdate();
if (this.el) {
this.el.innerHTML = this.render(); // 重新渲染
this._bindEvents && this._bindEvents();
}
this.updated && this.updated();
return this;
};
Component.prototype.destroy = function() {
if (this._destroyed) return;
this.beforeDestroy && this.beforeDestroy();
// 清理所有事件监听器
for (const [el, type, handler] of Object.values(this._listeners)) {
el.removeEventListener(type, handler);
}
if (this.el) this.el.innerHTML = '';
this._destroyed = true;
this.destroyed && this.destroyed();
};
// 工具方法:安全地为 DOM 添加事件(统一管理,销毁时自动清理)
Component.prototype.$on = function(el, type, handler) {
const key = `${type}_${Date.now()}`;
this._listeners[key] = [el, type, handler];
el.addEventListener(type, handler);
return this;
};
// ── 具体组件:计数器 ──
function CounterComponent(options) {
Component.call(this, {
...options,
data: { count: options.initialCount || 0, step: options.step || 1 }
});
}
// 建立原型链
CounterComponent.prototype = Object.create(Component.prototype);
CounterComponent.prototype.constructor = CounterComponent;
// 实现必要方法
CounterComponent.prototype.render = function() {
const { count, step } = this.data;
return `
<div class="counter" style="font-family:sans-serif;text-align:center;padding:20px;">
<h3 style="margin-bottom:16px;">计数器组件</h3>
<button id="dec" style="padding:8px 16px;font-size:16px;margin:0 8px;">-${step}</button>
<span style="font-size:32px;font-weight:bold;min-width:60px;display:inline-block;">
${count}
</span>
<button id="inc" style="padding:8px 16px;font-size:16px;margin:0 8px;">+${step}</button>
<button id="reset" style="padding:8px 12px;margin-left:16px;color:#999;">重置</button>
</div>
`;
};
CounterComponent.prototype._bindEvents = function() {
const { step } = this.data;
this.$on(this.el.querySelector('#inc'), 'click', () => this.update({ count: this.data.count + step }));
this.$on(this.el.querySelector('#dec'), 'click', () => this.update({ count: this.data.count - step }));
this.$on(this.el.querySelector('#reset'), 'click', () => this.update({ count: 0 }));
};
CounterComponent.prototype.mounted = function() {
console.log('[CounterComponent] 已挂载,初始值:', this.data.count);
};
CounterComponent.prototype.updated = function() {
console.log('[CounterComponent] 数据更新,当前值:', this.data.count);
};
// ── 具体组件:通知横幅 ──
function BannerComponent(options) {
Component.call(this, {
...options,
data: { message: options.message || '', type: options.type || 'info', visible: true }
});
}
BannerComponent.prototype = Object.create(Component.prototype);
BannerComponent.prototype.constructor = BannerComponent;
BannerComponent.prototype.render = function() {
const { message, type, visible } = this.data;
if (!visible) return '';
const colors = {
info: { bg: '#e8f4fd', border: '#1a73e8', text: '#1a73e8' },
success: { bg: '#e6f9ee', border: '#34a853', text: '#34a853' },
warning: { bg: '#fff8e6', border: '#fbbc04', text: '#e67e00' },
error: { bg: '#fde8e6', border: '#ea4a36', text: '#ea4a36' },
};
const c = colors[type] || colors.info;
return `
<div style="padding:12px 16px;border-left:4px solid ${c.border};
background:${c.bg};color:${c.text};border-radius:0 4px 4px 0;
display:flex;justify-content:space-between;align-items:center;">
<span>${message}</span>
<button id="close" style="background:none;border:none;cursor:pointer;
color:${c.text};font-size:18px;line-height:1;">×</button>
</div>
`;
};
BannerComponent.prototype._bindEvents = function() {
const closeBtn = this.el.querySelector('#close');
if (closeBtn) {
this.$on(closeBtn, 'click', () => {
this.update({ visible: false });
this.emit && this.emit('close');
});
}
};
// ── 实际使用 ──
// (在浏览器环境中运行)
// const counter = new CounterComponent({ initialCount: 0, step: 2 });
// counter.mount('#app');
// const banner = new BannerComponent({ message: '系统维护通知:今晚 22:00~23:00 停机维护', type: 'warning' });
// banner.mount('#notice');
// 验证继承
const counter = new CounterComponent({ initialCount: 10 });
console.log(counter instanceof CounterComponent); // true
console.log(counter instanceof Component); // true
console.log(typeof counter.mount); // 'function'(来自基类原型)
console.log(typeof counter.render); // 'function'(子类自己定义)
【代码注释】
核心逻辑(模板方法模式)
- 基类
Component:mount→render(子类实现)→_bindEvents→mounted;update合并data后重渲染;destroy清 DOM 与监听器。 - 子类
CounterComponent:Component.call(this, options)初始化实例属性;Object.create(Component.prototype)继承方法;只重写render、_bindEvents、生命周期钩子。
关键实现
this.mounted && this.mounted():可选钩子,子类不写不报错。$on将[el, type, handler]存入_listeners,destroy时removeEventListener,避免重复mount泄漏。update后再次_bindEvents:因innerHTML替换导致旧节点失效,需重新绑定(真实框架用虚拟 DOM diff 优化)。
注意点
- 字符串模板
render仅教学;生产用 VDOM/编译模板。 BannerComponent的this.emit需自行混入 EventEmitter 才有实现。
实战场景
- Vue 2 选项 API 生命周期、React class 组件
componentDidMount、小程序 Component 构造器。
真实网站场景: Vue 2 的 vm._init()、React 的 Component 基类,都采用了类似的"基类定义框架流程,子类实现业务细节"的模板方法模式。
6.6 事件循环实战:请求并发控制
场景: 批量上传文件、批量操作 API 时,如果同时发送 1000 个请求,会耗尽连接池、触发服务端限流。需要并发控制池——最多同时 N 个请求在运行。
/**
* 并发请求控制器
* 限制同时进行的异步任务数量,超出限制的任务排队等待。
*/
class ConcurrentController {
constructor(limit) {
this.limit = limit; // 最大并发数
this._active = 0; // 当前活跃任务数
this._queue = []; // 等待队列
}
/**
* 执行一个异步任务(自动排队控制)
* @param {Function} taskFn - 返回 Promise 的任务函数
*/
run(taskFn) {
return new Promise((resolve, reject) => {
// 任务包装:执行 + 完成后通知调度器
const execute = () => {
this._active++;
Promise.resolve(taskFn())
.then(resolve, reject)
.finally(() => {
this._active--;
this._next(); // 任务完成,激活队列中的下一个
});
};
if (this._active < this.limit) {
execute(); // 未达上限,立即执行
} else {
this._queue.push(execute); // 达到上限,排队等待
}
});
}
_next() {
if (this._queue.length > 0 && this._active < this.limit) {
const next = this._queue.shift();
next();
}
}
get stats() {
return {
active: this._active,
queued: this._queue.length,
limit: this.limit
};
}
}
// ── 使用示例:批量上传文件 ──
async function batchUpload(files) {
const controller = new ConcurrentController(3); // 最多同时上传 3 个文件
const results = [];
const uploadFile = (file) => () =>
new Promise((resolve, reject) => {
// 模拟上传耗时(真实场景替换为 fetch)
const duration = Math.random() * 2000 + 500;
setTimeout(() => {
if (Math.random() > 0.1) {
resolve({ file: file.name, status: 'success', url: `/uploads/${file.name}` });
} else {
reject(new Error(`${file.name} 上传失败`));
}
}, duration);
});
const tasks = files.map(file =>
controller.run(uploadFile(file))
.then(result => {
console.log(`✓ ${result.file} 上传成功`);
results.push(result);
})
.catch(err => {
console.error(`✗ ${err.message}`);
results.push({ file: file.name, status: 'failed', error: err.message });
})
);
await Promise.all(tasks);
return results;
}
// 模拟 10 个文件
const mockFiles = Array.from({ length: 10 }, (_, i) => ({ name: `document_${i + 1}.pdf` }));
batchUpload(mockFiles).then(results => {
console.log('\n上传汇总:');
const success = results.filter(r => r.status === 'success').length;
console.log(`成功: ${success}/${results.length}`);
});
// ── 使用示例:API 限流场景(每秒最多 5 个请求)──
const apiController = new ConcurrentController(5);
async function batchFetchUsers(userIds) {
const fetchUser = (id) => () =>
fetch(`/api/users/${id}`).then(r => r.json());
return Promise.all(
userIds.map(id => apiController.run(fetchUser(id)))
);
}
【代码注释】
核心逻辑
limit为最大同时运行数;run(taskFn)返回新 Promise,taskFn为返回 Promise 的函数(延迟创建,避免一入队就发请求)。_active < limit时立即execute();否则把execute函数推入_queue(不能推 Promise,否则已启动)。finally里_active--并_next():成功失败都释放槽位,防止死锁。
调度流程
run(A) run(B) run(C) // limit=3 → 三个立即执行
run(D) // 入队
A 完成 → shift D 执行
注意点
- 与「防抖/节流」不同:控制的是并行度,不是触发频率。
Promise.all等待全部结束;若需「完成一个补一个」可用p-limit或手写循环 +run。
实战场景
- 批量上传 OSS、爬虫限 QPS、数据库连接池、前端同时最多 N 个
fetch。
真实网站场景: 阿里云 OSS SDK 的批量上传、Axios 的请求拦截器实现限流,都有类似的并发控制逻辑。p-limit 是 npm 上实现相同功能的知名库(每周下载量超 1 亿次)。
6.7 事件循环实战:分时渲染长列表
场景: 一次性渲染 10000 条 DOM 节点会造成主线程长时间阻塞(页面白屏/卡顿)。利用事件循环的"宏任务分片",将渲染拆散到多个 tick 中,保持页面流畅。
/**
* 分时渲染(Time Slicing)
* 将大批量 DOM 操作分成小批次,通过 setTimeout 让出主线程,避免 UI 卡顿。
*/
function renderInChunks(container, items, options = {}) {
const {
chunkSize = 100, // 每批渲染数量
delay = 0, // 批次间隔(毫秒)
renderItem, // 渲染函数:(item, index) => HTMLString
onProgress, // 进度回调
onComplete // 完成回调
} = options;
let index = 0;
let fragment = document.createDocumentFragment(); // 离屏 DOM
function renderChunk() {
const end = Math.min(index + chunkSize, items.length);
// 批量创建 DOM(先在内存中操作,减少重排)
while (index < end) {
const el = document.createElement('div');
el.innerHTML = renderItem(items[index], index);
fragment.appendChild(el.firstElementChild);
index++;
}
// 一次性写入真实 DOM
container.appendChild(fragment);
fragment = document.createDocumentFragment();
// 进度回调
onProgress && onProgress(index, items.length);
if (index < items.length) {
// 通过 setTimeout 让出主线程,允许浏览器响应用户操作
setTimeout(renderChunk, delay);
} else {
onComplete && onComplete();
}
}
renderChunk();
}
// ── 更现代的方式:使用 requestIdleCallback ──
function renderWithIdleCallback(container, items, renderItem) {
let index = 0;
function processChunk(deadline) {
// 在浏览器空闲帧内尽量多渲染,剩余时间不足 5ms 时暂停
while (index < items.length && deadline.timeRemaining() > 5) {
const el = document.createElement('li');
el.innerHTML = renderItem(items[index], index);
container.appendChild(el);
index++;
}
if (index < items.length) {
requestIdleCallback(processChunk); // 下一个空闲帧继续
}
}
requestIdleCallback(processChunk);
}
// ── 虚拟列表(Virtual List)——终极方案 ──
/**
* 虚拟列表:只渲染可视区域内的 DOM,其他用占位替代
* 无论多少数据,DOM 节点数量恒定(只有可视区域的节点数 + 缓冲节点)
*/
function VirtualList(container, options) {
this.container = container;
this.items = options.items || [];
this.itemHeight = options.itemHeight || 40;
this.bufferSize = options.bufferSize || 5;
this.renderItem = options.renderItem;
this.containerHeight = container.clientHeight;
this.visibleCount = Math.ceil(this.containerHeight / this.itemHeight) + this.bufferSize * 2;
this.startIndex = 0;
this.totalHeight = this.items.length * this.itemHeight;
this._init();
}
VirtualList.prototype._init = function() {
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
// 占位 div:撑开滚动高度
this._phantom = document.createElement('div');
this._phantom.style.height = this.totalHeight + 'px';
this.container.appendChild(this._phantom);
// 实际渲染区域(绝对定位,随滚动偏移)
this._content = document.createElement('div');
this._content.style.position = 'absolute';
this._content.style.top = '0';
this._content.style.width = '100%';
this.container.appendChild(this._content);
// 绑定滚动事件
this.container.addEventListener('scroll', () => this._onScroll());
this._render();
};
VirtualList.prototype._onScroll = function() {
const scrollTop = this.container.scrollTop;
const newStart = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferSize);
if (newStart !== this.startIndex) {
this.startIndex = newStart;
this._render();
}
};
VirtualList.prototype._render = function() {
const endIndex = Math.min(this.startIndex + this.visibleCount, this.items.length);
const offsetY = this.startIndex * this.itemHeight;
// 偏移内容区,模拟已滚过部分的高度
this._content.style.transform = `translateY(${offsetY}px)`;
// 只渲染可视范围内的节点
this._content.innerHTML = this.items
.slice(this.startIndex, endIndex)
.map((item, i) => this.renderItem(item, this.startIndex + i))
.join('');
};
// ── 演示:分时渲染 10000 条数据 ──
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
title: `列表项 ${i + 1}`,
desc: `这是第 ${i + 1} 条数据的描述内容`
}));
// 分时渲染(适合不定高列表、有复杂交互的场景)
const container = document.getElementById('list-container');
if (container) {
renderInChunks(container, items, {
chunkSize: 200,
renderItem: (item) => `
<div style="padding:12px;border-bottom:1px solid #eee;">
<strong>${item.title}</strong>
<p style="color:#666;font-size:13px;margin-top:4px;">${item.desc}</p>
</div>
`,
onProgress: (current, total) => {
console.log(`渲染进度: ${current}/${total} (${Math.round(current/total*100)}%)`);
},
onComplete: () => console.log('全部渲染完成!')
});
}
【代码注释】
方案一:分时渲染 renderInChunks
- 每批处理
chunkSize条,写入DocumentFragment后一次性appendChild,减少重排/reflow。 setTimeout(renderChunk, delay):下一批进入宏任务,让出主线程处理输入、绘制,避免 1 万条同步插入卡死。
方案二:requestIdleCallback
- 在浏览器空闲帧内执行,
deadline.timeRemaining() > 5才继续,比盲目setTimeout(0)更不伤 60fps。
方案三:虚拟列表 VirtualList
_phantom撑开总高度;仅渲染visibleCount条 +bufferSize,translateY模拟滚动偏移。- 数据量 ∞ 时 DOM 数量仍 ≈ 视口条数,是 react-virtual、vue-virtual-scroller 的核心。
选型建议
| 场景 | 推荐 |
|---|---|
| 万条简单列表、快速落地 | 分时 + Fragment |
| 低优先级后台渲染 | rIC |
| 超长列表、固定行高 | 虚拟列表 |
实战场景
- 微博/知乎 Feed、飞书文档、后台表格万行数据。
真实网站场景: 微博/知乎的无限滚动列表、飞书文档的长文档渲染,均采用虚拟列表技术。
6.8 Web Worker 实战:CSV 解析与数据聚合
场景: 用户上传 10MB 的 CSV 文件,浏览器端解析 + 数据统计——如果在主线程执行,会导致 UI 冻结数秒。用 Worker 放到后台线程执行。
主线程(main.js):
/**
* CSV 数据分析器:利用 Worker 在后台线程解析和聚合数据
*/
class CSVAnalyzer {
constructor() {
// 使用 Blob URL 创建内联 Worker(无需单独的 worker 文件)
this.worker = this._createWorker();
this._callbacks = new Map();
this._taskId = 0;
this.worker.onmessage = (e) => {
const { taskId, type, data, error, progress } = e.data;
if (type === 'progress') {
const cb = this._callbacks.get(`${taskId}_progress`);
cb && cb(progress);
return;
}
const resolve = this._callbacks.get(`${taskId}_resolve`);
const reject = this._callbacks.get(`${taskId}_reject`);
if (error) {
reject && reject(new Error(error));
} else {
resolve && resolve(data);
}
// 清理回调
this._callbacks.delete(`${taskId}_resolve`);
this._callbacks.delete(`${taskId}_reject`);
this._callbacks.delete(`${taskId}_progress`);
};
this.worker.onerror = (e) => {
console.error('Worker 错误:', e.message);
};
}
_createWorker() {
// 将 Worker 代码内联为 Blob(方便演示,无需外部文件)
const workerCode = `
${workerScript.toString()}
workerScript();
`;
const blob = new Blob([`(${workerScript.toString()})()`], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
_send(type, payload, onProgress) {
return new Promise((resolve, reject) => {
const taskId = ++this._taskId;
this._callbacks.set(`${taskId}_resolve`, resolve);
this._callbacks.set(`${taskId}_reject`, reject);
if (onProgress) this._callbacks.set(`${taskId}_progress`, onProgress);
this.worker.postMessage({ taskId, type, ...payload });
});
}
// 解析 CSV 文本
parse(csvText, onProgress) {
return this._send('parse', { csvText }, onProgress);
}
// 按列聚合统计
aggregate(rows, groupBy, metrics) {
return this._send('aggregate', { rows, groupBy, metrics });
}
destroy() {
this.worker.terminate();
}
}
// ── Worker 内部逻辑(实际项目中放到单独的 worker.js 文件)──
function workerScript() {
self.onmessage = function(e) {
const { taskId, type } = e.data;
try {
if (type === 'parse') {
const result = parseCSV(e.data.csvText, (progress) => {
self.postMessage({ taskId, type: 'progress', progress });
});
self.postMessage({ taskId, type: 'done', data: result });
} else if (type === 'aggregate') {
const result = aggregateData(e.data.rows, e.data.groupBy, e.data.metrics);
self.postMessage({ taskId, type: 'done', data: result });
}
} catch (err) {
self.postMessage({ taskId, type: 'error', error: err.message });
}
};
function parseCSV(text, onProgress) {
const lines = text.split('\n').filter(Boolean);
const headers = lines[0].split(',').map(h => h.trim());
const rows = [];
const total = lines.length - 1;
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',');
const row = {};
headers.forEach((h, j) => {
const val = values[j] ? values[j].trim() : '';
row[h] = isNaN(val) || val === '' ? val : Number(val);
});
rows.push(row);
// 每处理 1000 行报告一次进度
if (i % 1000 === 0) {
onProgress(Math.round(i / total * 100));
}
}
return { headers, rows, total };
}
function aggregateData(rows, groupBy, metrics) {
const groups = {};
rows.forEach(row => {
const key = row[groupBy] || 'unknown';
if (!groups[key]) {
groups[key] = { count: 0 };
metrics.forEach(m => { groups[key][m] = { sum: 0, min: Infinity, max: -Infinity }; });
}
groups[key].count++;
metrics.forEach(m => {
if (typeof row[m] === 'number') {
groups[key][m].sum += row[m];
groups[key][m].min = Math.min(groups[key][m].min, row[m]);
groups[key][m].max = Math.max(groups[key][m].max, row[m]);
}
});
});
// 计算平均值
return Object.entries(groups).map(([key, stats]) => {
const result = { [groupBy]: key, count: stats.count };
metrics.forEach(m => {
result[`${m}_avg`] = (stats[m].sum / stats.count).toFixed(2);
result[`${m}_min`] = stats[m].min;
result[`${m}_max`] = stats[m].max;
});
return result;
}).sort((a, b) => b.count - a.count);
}
}
// ── 主线程使用示例 ──
async function analyzeUploadedCSV(file) {
const analyzer = new CSVAnalyzer();
const progressBar = document.querySelector('#progress');
console.log(`开始解析:${file.name}(${(file.size / 1024 / 1024).toFixed(2)} MB)`);
console.log('主线程保持响应,可以继续操作 UI...');
try {
// 读取文件
const text = await file.text();
// 在 Worker 中解析(不阻塞主线程)
const { rows, total } = await analyzer.parse(text, (progress) => {
if (progressBar) progressBar.value = progress;
console.log(`解析进度:${progress}%`);
});
console.log(`解析完成,共 ${total} 条记录`);
// 在 Worker 中聚合统计
const stats = await analyzer.aggregate(rows, 'category', ['price', 'quantity']);
console.log('分类统计结果:', stats);
return stats;
} finally {
analyzer.destroy();
}
}
【代码注释】
核心逻辑
CSVAnalyzer用Blob+URL.createObjectURL内联 Worker,免单独worker.js;生产建议独立文件便于调试与缓存。_send递增taskId,在_callbacksMap 存resolve/reject/progress;Worker 回包带taskId路由到对应 Promise。- Worker 内
parseCSV按行解析,i % 1000发progress;aggregateData按groupBy分组算 sum/min/max/avg。
主线程与 Worker 分工
| 主线程 | Worker |
|---|---|
读文件 file.text()、更新 UI、进度条 |
大字符串 split、循环聚合 |
postMessage / onmessage |
无 DOM,可全力算 |
注意点
postMessage大对象有拷贝成本;超大 CSV 可考虑分片传输或Transferable。finally里analyzer.destroy()调用terminate()释放线程。- 示例 CSV 用逗号 split,未处理引号内逗号,生产需完整解析库。
实战场景
- 后台导出 Excel 预览、埋点日志本地分析、地图瓦片离线计算。
6.9 综合实战:虚拟任务调度器
将闭包、原型链、事件循环三个核心知识点融合到一个"微型任务调度器"中,模拟 React Scheduler 的核心思想。
/**
* 微型任务调度器
* 融合:闭包(私有状态)+ 原型链(方法共享)+ 事件循环(宏任务分片)
*/
function Scheduler(options = {}) {
// 闭包私有配置
const frameTime = options.frameTime || 5; // 每帧最长执行时间(ms)
// 实例属性
this._queue = []; // 任务优先级队列
this._running = false;
this._paused = false;
this._stats = { completed: 0, skipped: 0, total: 0 };
}
// ── 原型方法 ──
/** 添加任务(支持优先级) */
Scheduler.prototype.schedule = function(task, priority = 3) {
const job = {
id: ++this._stats.total,
fn: task,
priority, // 1=高 2=普通 3=低
createdAt: performance.now(),
timeout: priority === 1 ? 250 : priority === 2 ? 5000 : Infinity
};
// 按优先级插入(简单实现,生产用堆排序)
const insertIndex = this._queue.findIndex(j => j.priority > priority);
if (insertIndex === -1) {
this._queue.push(job);
} else {
this._queue.splice(insertIndex, 0, job);
}
if (!this._running && !this._paused) {
this._run();
}
return job.id;
};
/** 主循环:利用 setTimeout 实现协作式调度 */
Scheduler.prototype._run = function() {
if (this._paused || this._queue.length === 0) {
this._running = false;
return;
}
this._running = true;
const deadline = performance.now() + 5; // 本帧截止时间(5ms)
while (this._queue.length > 0 && performance.now() < deadline) {
const job = this._queue.shift();
const now = performance.now();
// 检查任务是否超时(过期任务降级处理)
if (now - job.createdAt > job.timeout) {
this._stats.skipped++;
console.warn(`[Scheduler] 任务 #${job.id} 超时跳过`);
continue;
}
try {
job.fn();
this._stats.completed++;
} catch (err) {
console.error(`[Scheduler] 任务 #${job.id} 执行出错:`, err.message);
}
}
if (this._queue.length > 0) {
// 还有未完成任务,通过宏任务让出主线程,下一轮继续
setTimeout(() => this._run(), 0);
} else {
this._running = false;
console.log('[Scheduler] 所有任务完成', this._stats);
}
};
Scheduler.prototype.pause = function() { this._paused = true; };
Scheduler.prototype.resume = function() {
this._paused = false;
if (!this._running) this._run();
};
Scheduler.prototype.clear = function() { this._queue = []; this._running = false; };
Scheduler.prototype.getStats = function() { return { ...this._stats, queued: this._queue.length }; };
// ── 使用示例 ──
const scheduler = new Scheduler({ frameTime: 5 });
// 批量添加不同优先级的任务
for (let i = 0; i < 5; i++) {
scheduler.schedule(() => {
// 模拟 DOM 更新(低优先级)
console.log(`[低优先级] 渲染任务 ${i + 1}`);
}, 3);
}
// 高优先级任务插队
scheduler.schedule(() => {
console.log('[高优先级] 用户点击响应(立即执行)');
}, 1);
scheduler.schedule(() => {
console.log('[普通优先级] 数据请求回调');
}, 2);
// 输出顺序:高优先级 → 普通优先级 → 低优先级(顺序)
【代码注释】
核心逻辑
_run每轮用performance.now() + 5作为时间预算,在while中shift执行任务;队列未空则setTimeout(() => this._run(), 0)让出主线程(协作式调度)。schedule(task, priority)按priority数值插入_queue(数字越小越靠前),高优任务先执行。job.timeout:等待过久则skipped++并跳过,避免低优任务长期占用队列。
关键 API
performance.now():高精度时间,适合帧调度与性能测量。setTimeout(..., 0):让出主线程,与事件循环、React Fiber 时间切片同源思路。
注意点
- 真实 React Scheduler 更复杂(Lane、Concurrent Mode);此处为教学简化版。
- 长列表 + Worker + 并发控制 + 调度器组合,即本章知识点的工程化串联。
实战场景
- React 18 并发渲染、浏览器
requestIdleCallback、游戏主循环中的「固定时间片」逻辑。
参考资源
本文详细解析了JavaScript高级核心概念,包括原型链、继承机制、闭包、事件循环以及Web Worker多线程实现。通过深入理解这些概念,开发者可以更好地掌握JavaScript的运行机制,编写更高效、更专业的代码。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)