数据传递机制
Worker线程 主线程 Worker线程 主线程 接收数据 接收结果 postMessage(data) 执行计算 postMessage(result)

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) 通过结构化克隆传数组(非引用共享)。
  • onmessageevent.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 原型链总结

原型链

核心概念

Prototype

proto

Constructor

关系规则

函数有prototype

对象有__proto__

构造函数.prototype === 实例.proto

继承实现

子类.prototype = new 父类

子类.prototype.constructor = 子类

this, 参数

应用场景

方法共享

继承实现

扩展内置对象

5.2 事件循环机制总结

事件循环

执行顺序

同步代码

微任务队列

宏任务队列

任务类型

微任务

Promise.then

MutationObserver

宏任务

setTimeout

setInterval

DOM事件

Ajax回调

关键要点

主线程空闲才执行

先微后宏

单线程执行

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 延迟 delay ms 执行 fn——连续输入时只有最后一次停顿delay 才触发。
  • timer 存在闭包中,返回的包装函数多次调用共享同一 timer 变量,实现「取消上一次、预约下一次」。
  • debounceAdvancedimmediate && !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 闭包持有 cacheMap),外层 memoize 执行结束后 cache 仍存活——闭包延长变量生命周期的典型。
  • 命中:cache.has(key) 直接 get;未命中:fn.apply 计算后 setresolver 可自定义 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、Vue computed、接口层 dedupe、lodash memoize

真实网站场景: 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 对外不可见,仅暴露 addItemcheckout 等 API。
  • addItem 合并同 id 商品数量;setDiscount 校验 0~1;checkoutgetItems() 快照后 clear(),生成订单对象。

封装细节

  • return this:链式 ShoppingCart.addItem(...).setDiscount(0.1)
  • getItems() 返回 map 浅拷贝新对象,避免外部改 _items 内部引用;若需防改嵌套字段可再深拷贝。

注意点

  • ShoppingCart._itemsundefined——私有变量不在返回对象上,不是「下划线约定公开」。
  • 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 监听器;emitslice 复制数组再遍历,防止回调里 off 导致遍历时删元素。
  • once:包装 wrapper,首次执行后 off(event, wrapper)wrapper._originaloff 匹配原始函数。
  • UserStoreEventEmitter.call(this) 初始化 _eventsObject.create(EventEmitter.prototype) 继承原型方法;fetchUsersemit('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策略字典requiredphone 等返回 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'(子类自己定义)

【代码注释】

核心逻辑(模板方法模式)

  • 基类 Componentmountrender(子类实现)→ _bindEventsmountedupdate 合并 data 后重渲染;destroy 清 DOM 与监听器。
  • 子类 CounterComponentComponent.call(this, options) 初始化实例属性;Object.create(Component.prototype) 继承方法;只重写 render_bindEvents、生命周期钩子。

关键实现

  • this.mounted && this.mounted():可选钩子,子类不写不报错。
  • $on[el, type, handler] 存入 _listenersdestroyremoveEventListener,避免重复 mount 泄漏。
  • update 后再次 _bindEvents:因 innerHTML 替换导致旧节点失效,需重新绑定(真实框架用虚拟 DOM diff 优化)。

注意点

  • 字符串模板 render 仅教学;生产用 VDOM/编译模板。
  • BannerComponentthis.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 条 + bufferSizetranslateY 模拟滚动偏移。
  • 数据量 ∞ 时 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();
    }
}

【代码注释】

核心逻辑

  • CSVAnalyzerBlob + URL.createObjectURL 内联 Worker,免单独 worker.js;生产建议独立文件便于调试与缓存。
  • _send 递增 taskId,在 _callbacks Map 存 resolve/reject/progress;Worker 回包带 taskId 路由到对应 Promise。
  • Worker 内 parseCSV 按行解析,i % 1000progressaggregateDatagroupBy 分组算 sum/min/max/avg。

主线程与 Worker 分工

主线程 Worker
读文件 file.text()、更新 UI、进度条 大字符串 split、循环聚合
postMessage / onmessage 无 DOM,可全力算

注意点

  • postMessage 大对象有拷贝成本;超大 CSV 可考虑分片传输或 Transferable
  • finallyanalyzer.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 作为时间预算,在 whileshift 执行任务;队列未空则 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的运行机制,编写更高效、更专业的代码。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐