突破限制:实现页面内精准监听 localStorage 变更

一、简介

        localStorage 作为浏览器提供的本地存储 API,为开发者持久化保存客户端数据提供了便利。然而,它原生的事件监听机制存在一个关键限制:storage 事件仅在跨页面(同源的不同标签页或窗口)修改 localStorage 时触发,当前页面自身的修改则无法被监听到。这就形成了 “灯下黑” 现象,让开发者难以直接监测同一页面内的存储变更。
        虽然可以通过定时轮询来勉强实现监听,但这种方式问题颇多。它会持续消耗系统资源,犹如一个不知疲倦的 “消耗者”,而且存在监听延迟和遗漏风险,就像在捕捉快速移动的目标时,可能会错过关键瞬间。
        为解决这一核心痛点,业界已探索出多种高效解决方案。这些方法能够实现在页面内精准的实时监听。比如,利用 StorageEvent 监听 DOM 变化,间接判断 localStorage 的变动。当页面中的某些操作可能引发 localStorage 变化时,StorageEvent 能敏锐地察觉到,并及时做出响应。还有通过封装 localStorage 的操作方法,在每次修改时主动触发自定义事件,这种方式如同在 localStorage 的操作和监听之间建立了一座直接沟通的桥梁。这些解决方案为开发者提供了更多选择,让我们能够更灵活、高效地应对页面内 localStorage 变更的监听需求,为提升用户体验和应用程序的性能开辟了新的道路。它们就像是黑暗中的明灯,照亮了 localStorage 监听的 “灯下黑” 之处,让我们的开发工作更加顺畅。

二、示例演示

1.示例代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage同页面监听问题</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .box {
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        .log {
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>localStorage同页面监听问题演示</h1>

    <div class="box">
        <h2>操作面板</h2>
        <button id="setBtn">设置localStorage</button>
        <button id="removeBtn">移除localStorage</button>
        <button id="clearBtn">清空localStorage</button>
    </div>

    <div class="box">
        <h2>原生storage事件监听</h2>
        <p>这个区域显示原生storage事件的触发情况</p>
        <div id="nativeLog" class="log"></div>
    </div>

    <div class="box">
        <h2>同页面修改日志</h2>
        <p>这个区域显示当前页面对localStorage的直接操作</p>
        <div id="directLog" class="log"></div>
    </div>

    <div class="tips">
        <h3>问题说明:</h3>
        <p>1. 点击上方按钮修改localStorage</p>
        <p>2. 原生storage事件<strong>不会</strong>在同页面修改时触发</p>
        <p>3. 只有<strong>其他同源页面</strong>修改localStorage时才会触发</p>
        <p>4. 要测试原生事件,请打开另一个同源页面并修改localStorage</p>
    </div>
</div>

<script>
    // 获取DOM元素
    const setBtn = document.getElementById('setBtn');
    const removeBtn = document.getElementById('removeBtn');
    const clearBtn = document.getElementById('clearBtn');
    const nativeLog = document.getElementById('nativeLog');
    const directLog = document.getElementById('directLog');

    // 原生storage事件监听
    window.addEventListener('storage', (event) => {
        const logEntry = `
            [原生事件] ${new Date().toLocaleTimeString()}
            Key: ${event.key}
            Old Value: ${event.oldValue}
            New Value: ${event.newValue}
            URL: ${event.url}
            ------------------------------
            `;
        nativeLog.innerHTML += logEntry;
        nativeLog.scrollTop = nativeLog.scrollHeight;
    });

    // 设置localStorage
    setBtn.addEventListener('click', () => {
        const key = 'testKey';
        const value = `Value-${Date.now()}`;
        const oldValue = localStorage.getItem(key);

        localStorage.setItem(key, value);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            设置了: ${key} = ${value}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 移除localStorage
    removeBtn.addEventListener('click', () => {
        const key = 'testKey';
        const oldValue = localStorage.getItem(key);

        localStorage.removeItem(key);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            移除了: ${key}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 清空localStorage
    clearBtn.addEventListener('click', () => {
        localStorage.clear();

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            清空了所有localStorage数据
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });
</script>
</body>
</html>

2.将上述代码复制到test.html中,使用webstrom打开,点击设置localStorage,可以看到storage事件监听中并未触发
在这里插入图片描述
3.再将上述代码复制到test1.html中,使用webstrom打开,点击设置localStorage,可以看到此时的test.html已触发storage事件监听
在这里插入图片描述

三、StorageEvent重构setItem

StorageEvent 是 Web Storage API 的一部分,当存储区域(localStorage 或 sessionStorage)被其他文档修改时,会触发这个事件。
基本概念:

  • 跨文档通信:StorageEvent 主要用于在同一个源(origin)下的不同窗口或标签页之间通信
  • 触发条件:只有当其他窗口或标签页修改了存储时才会触发,当前窗口的修改不会触发
  • 监听方式:通过 window.addEventListener(‘storage’, callback) 监听

StorageEvent 对象包含以下重要属性:

  • key:被修改的键名。如果是调用 clear() 方法则为 null
  • oldValue:修改前的值。如果是新增项则为 null
  • newValue:修改后的值。如果是删除项则为 null
  • url:触发修改的文档的 URL
  • storageArea:被操作的存储对象(localStorage 或 sessionStorage)

1.在代码顶部加上下面图片中的代码
在这里插入图片描述
2.StorageEvent重构setItem代码

const originalSetItem = localStorage.setItem;
const originalRemoveItem = localStorage.removeItem;

// 重写 setItem,在存储数据时触发自定义事件
localStorage.setItem = function(key, value) {
    const event = new StorageEvent('storage', {
        key,
        oldValue: localStorage.getItem(key),
        newValue: value,
        storageArea: localStorage,
        url: window.location.href
    });

    originalSetItem.call(localStorage, key, value); // 调用原始方法
    window.dispatchEvent(event); // 手动触发事件
};

// 重写 removeItem,在移除数据时触发自定义事件
localStorage.removeItem = function(key) {
    const event = new StorageEvent('storage', {
        key,
        oldValue: localStorage.getItem(key),
        newValue: null,
        storageArea: localStorage,
        url: window.location.href
    });

    originalRemoveItem.call(localStorage, key); // 调用原始方法
    window.dispatchEvent(event); // 手动触发事件
};

3.实际效果
在这里插入图片描述

四、CustomEvent自定义事件同一页面不同模块数据同步

CustomEvent 是 JavaScript 中用于创建自定义事件的接口,继承自 Event,允许传递自定义数据(通过 detail 属性),常用于组件间通信或模拟原生事件。通过 new CustomEvent(type, options) 创建,options 可配置 bubbles(冒泡)、cancelable(可取消)及 detail(携带数据),随后通过 dispatchEvent 触发

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage同页面监听问题</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .box {
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        .log {
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>localStorage同页面监听问题演示</h1>

    <div class="box">
        <h2>操作面板</h2>
        <button id="setBtn">设置localStorage</button>
        <button id="removeBtn">移除localStorage</button>
        <button id="clearBtn">清空localStorage</button>
    </div>

    <div class="box">
        <h2>原生storage事件监听</h2>
        <p>这个区域显示原生storage事件的触发情况</p>
        <div id="nativeLog" class="log"></div>
    </div>

    <div class="box">
        <h2>同页面修改日志</h2>
        <p>这个区域显示当前页面对localStorage的直接操作</p>
        <div id="directLog" class="log"></div>
    </div>

    <div class="tips">
        <h3>问题说明:</h3>
        <p>1. 点击上方按钮修改localStorage</p>
        <p>2. 原生storage事件<strong>不会</strong>在同页面修改时触发</p>
        <p>3. 只有<strong>其他同源页面</strong>修改localStorage时才会触发</p>
        <p>4. 要测试原生事件,请打开另一个同源页面并修改localStorage</p>
    </div>
</div>

<script>
    // 获取DOM元素
    const setBtn = document.getElementById('setBtn');
    const removeBtn = document.getElementById('removeBtn');
    const clearBtn = document.getElementById('clearBtn');
    const nativeLog = document.getElementById('nativeLog');
    const directLog = document.getElementById('directLog');

    const createLocalStorageEvent = (key, oldValue, newValue, type) => {
        return new CustomEvent('localStorageChange',{
            detail:{
                key,
                oldValue,
                newValue,
                type,
                timestamp: Date.now(),
                url: window.location.href
            }
        });
    };

    window.addEventListener('localStorageChange', (event) => {
        const logEntry = `
            [Proxy监听] ${new Date().toLocaleTimeString()}
            操作类型: ${event.detail.type}
            Key: ${event.detail.key}
            Old Value: ${event.detail.oldValue}
            New Value: ${event.detail.newValue}
            ------------------------------
            `;
        nativeLog.innerHTML += logEntry;
        nativeLog.scrollTop = nativeLog.scrollHeight;
    });

    const originalSetItem = localStorage.setItem;
    const originalRemoveItem = localStorage.removeItem;

    // 重写 setItem,在存储数据时触发自定义事件
    localStorage.setItem = function(key, value) {

        originalSetItem.call(localStorage, key, value); // 调用原始方法
        window.dispatchEvent(createLocalStorageEvent(key, localStorage.getItem(key), value, 'setItem')); // 手动触发事件
    };

    // 重写 removeItem,在移除数据时触发自定义事件
    localStorage.removeItem = function(key) {

        originalRemoveItem.call(localStorage, key); // 调用原始方法
        window.dispatchEvent(createLocalStorageEvent(key, localStorage.getItem(key), null, 'removeItem')); // 手动触发事件

    };

    // 原生storage事件监听
    // window.addEventListener('storage', (event) => {
    //     const logEntry = `
    //         [原生事件] ${new Date().toLocaleTimeString()}
    //         Key: ${event.key}
    //         Old Value: ${event.oldValue}
    //         New Value: ${event.newValue}
    //         URL: ${event.url}
    //         ------------------------------
    //         `;
    //     nativeLog.innerHTML += logEntry;
    //     nativeLog.scrollTop = nativeLog.scrollHeight;
    // });

    // 设置localStorage
    setBtn.addEventListener('click', () => {
        const key = 'testKey';
        const value = `Value-${Date.now()}`;
        const oldValue = localStorage.getItem(key);

        localStorage.setItem(key, value);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            设置了: ${key} = ${value}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 移除localStorage
    removeBtn.addEventListener('click', () => {
        const key = 'testKey';
        const oldValue = localStorage.getItem(key);

        localStorage.removeItem(key);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            移除了: ${key}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 清空localStorage
    clearBtn.addEventListener('click', () => {
        localStorage.clear();

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            清空了所有localStorage数据
            ------------------------------
            `;![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3087bfff6e4b4feebaf9d6456ec59d9a.gif#pic_center)

        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });
</script>
</body>
</html>

在这里插入图片描述

五、MessageChannel同一页面不同模块数据同步

MessageChannel 是浏览器提供的双向通信管道,创建一对相互关联的 MessagePort(port1 和 port2),允许不同上下文(如窗口、Worker、iframe)直接传输数据,实现点对点高效通信,无需依赖广播或全局事件。通过 postMessage 发送消息,onmessage 接收,适用于跨线程任务分发或框架内部通信,性能优于 postMessage 的全局通信。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage同页面监听问题</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .box {
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        .log {
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>localStorage同页面监听问题演示</h1>

    <div class="box">
        <h2>操作面板</h2>
        <button id="setBtn">设置localStorage</button>
        <button id="removeBtn">移除localStorage</button>
        <button id="clearBtn">清空localStorage</button>
    </div>

    <div class="box">
        <h2>原生storage事件监听</h2>
        <p>这个区域显示原生storage事件的触发情况</p>
        <div id="nativeLog" class="log"></div>
    </div>

    <div class="box">
        <h2>同页面修改日志</h2>
        <p>这个区域显示当前页面对localStorage的直接操作</p>
        <div id="directLog" class="log"></div>
    </div>

    <div class="tips">
        <h3>问题说明:</h3>
        <p>1. 点击上方按钮修改localStorage</p>
        <p>2. 原生storage事件<strong>不会</strong>在同页面修改时触发</p>
        <p>3. 只有<strong>其他同源页面</strong>修改localStorage时才会触发</p>
        <p>4. 要测试原生事件,请打开另一个同源页面并修改localStorage</p>
    </div>
</div>

<script>
    // 获取DOM元素
    const setBtn = document.getElementById('setBtn');
    const removeBtn = document.getElementById('removeBtn');
    const clearBtn = document.getElementById('clearBtn');
    const nativeLog = document.getElementById('nativeLog');
    const directLog = document.getElementById('directLog');

    // 创建 MessageChannel
    const channel = new MessageChannel();
    const port1 = channel.port1;
    const port2 = channel.port2;

    // 存储原始方法
    const originalSetItem = localStorage.setItem;
    const originalRemoveItem = localStorage.removeItem;
    const originalClear = localStorage.clear;

    // 重写 localStorage 方法
    localStorage.setItem = function(key, value) {
        const oldValue = localStorage.getItem(key);
        originalSetItem.call(localStorage, key, value);

        // 通过 MessageChannel 发送消息
        port1.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: value,
            action: 'set'
        });
    };

    localStorage.removeItem = function(key) {
        const oldValue = localStorage.getItem(key);
        originalRemoveItem.call(localStorage, key);

        port1.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: null,
            action: 'remove'
        });
    };

    localStorage.clear = function() {
        originalClear.call(localStorage);

        port1.postMessage({
            type: 'storage',
            action: 'clear'
        });
    };

    // 监听 MessageChannel 消息
    port2.onmessage = function(event) {
        const { type, key, oldValue, newValue, action } = event.data;

        if (type === 'storage') {
            let message = '';
            switch (action) {
                case 'set':
                    message = `Key "${key}" 被修改: ${oldValue}${newValue}`;
                    break;
                case 'remove':
                    message = `Key "${key}" 被移除 (旧值: ${oldValue})`;
                    break;
                case 'clear':
                    message = 'localStorage 已清空';
                    break;
            }

            const logEntry = `
            [原生事件] ${new Date().toLocaleTimeString()}
            Key: ${event.key}
            Old Value: ${event.oldValue}
            New Value: ${event.newValue}
            URL: ${event.url}
            message : ${message}
            ------------------------------
            `;
            nativeLog.innerHTML += logEntry;
            nativeLog.scrollTop = nativeLog.scrollHeight;
        }
    };

    // 启动端口监听
    port2.start();

    // // 原生storage事件监听
    // window.addEventListener('storage', (event) => {
    //     const logEntry = `
    //         [原生事件] ${new Date().toLocaleTimeString()}
    //         Key: ${event.key}
    //         Old Value: ${event.oldValue}
    //         New Value: ${event.newValue}
    //         URL: ${event.url}
    //         ------------------------------
    //         `;
    //     nativeLog.innerHTML += logEntry;
    //     nativeLog.scrollTop = nativeLog.scrollHeight;
    // });

    // 设置localStorage
    setBtn.addEventListener('click', () => {
        const key = 'testKey';
        const value = `Value-${Date.now()}`;
        const oldValue = localStorage.getItem(key);

        localStorage.setItem(key, value);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            设置了: ${key} = ${value}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 移除localStorage
    removeBtn.addEventListener('click', () => {
        const key = 'testKey';
        const oldValue = localStorage.getItem(key);

        localStorage.removeItem(key);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            移除了: ${key}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 清空localStorage
    clearBtn.addEventListener('click', () => {
        localStorage.clear();

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            清空了所有localStorage数据
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });
</script>
</body>
</html>

在这里插入图片描述

六、BroadcastChannel多窗口数据同步

BroadcastChannel 是浏览器提供的跨页面通信API,允许同源下的不同窗口、标签页、iframe或Worker通过共享频道名实时广播和接收消息(如postMessage发送、onmessage监听),适用于多页数据同步(如登录状态、实时通知),无需依赖服务端或局部存储,通信高效且自动管理连接,关闭页面或调用close()即可释放资源

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage同页面监听问题</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .box {
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        .log {
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>localStorage同页面监听问题演示</h1>

    <div class="box">
        <h2>操作面板</h2>
        <button id="setBtn">设置localStorage</button>
        <button id="removeBtn">移除localStorage</button>
        <button id="clearBtn">清空localStorage</button>
    </div>

    <div class="box">
        <h2>原生storage事件监听</h2>
        <p>这个区域显示原生storage事件的触发情况</p>
        <div id="nativeLog" class="log"></div>
    </div>

    <div class="box">
        <h2>同页面修改日志</h2>
        <p>这个区域显示当前页面对localStorage的直接操作</p>
        <div id="directLog" class="log"></div>
    </div>

    <div class="tips">
        <h3>问题说明:</h3>
        <p>1. 点击上方按钮修改localStorage</p>
        <p>2. 原生storage事件<strong>不会</strong>在同页面修改时触发</p>
        <p>3. 只有<strong>其他同源页面</strong>修改localStorage时才会触发</p>
        <p>4. 要测试原生事件,请打开另一个同源页面并修改localStorage</p>
    </div>
</div>

<script>
    // 获取DOM元素
    const setBtn = document.getElementById('setBtn');
    const removeBtn = document.getElementById('removeBtn');
    const clearBtn = document.getElementById('clearBtn');
    const nativeLog = document.getElementById('nativeLog');
    const directLog = document.getElementById('directLog');

    // 创建 BroadcastChannel
    const storageChannel = new BroadcastChannel('local_storage_channel');

    // 存储原始方法
    const originalSetItem = localStorage.setItem;
    const originalRemoveItem = localStorage.removeItem;
    const originalClear = localStorage.clear;

    // 重写 localStorage 方法
    localStorage.setItem = function(key, value) {
        const oldValue = localStorage.getItem(key);
        originalSetItem.call(localStorage, key, value);

        // 通过 BroadcastChannel 广播消息
        storageChannel.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: value,
            action: 'set'
        });
    };

    localStorage.removeItem = function(key) {
        const oldValue = localStorage.getItem(key);
        originalRemoveItem.call(localStorage, key);

        storageChannel.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: null,
            action: 'remove'
        });
    };

    localStorage.clear = function() {
        originalClear.call(localStorage);

        storageChannel.postMessage({
            type: 'storage',
            action: 'clear'
        });
    };

    // 监听 BroadcastChannel 消息
    storageChannel.onmessage = function(event) {
        const { type, key, oldValue, newValue, action } = event.data;

        if (type === 'storage') {
            let message = '';
            switch (action) {
                case 'set':
                    message = `Key "${key}" 被修改: ${oldValue}${newValue}`;
                    break;
                case 'remove':
                    message = `Key "${key}" 被移除 (旧值: ${oldValue})`;
                    break;
                case 'clear':
                    message = 'localStorage 已清空';
                    break;
            }

            const logEntry = `
            [原生事件] ${new Date().toLocaleTimeString()}
            Key: ${event.key}
            Old Value: ${event.oldValue}
            New Value: ${event.newValue}
            URL: ${event.url}
            message : ${message}
            ------------------------------
            `;
            nativeLog.innerHTML += logEntry;
            nativeLog.scrollTop = nativeLog.scrollHeight;
        }
    };

    // 原生storage事件监听
    // window.addEventListener('storage', (event) => {
    //     const logEntry = `
    //         [原生事件] ${new Date().toLocaleTimeString()}
    //         Key: ${event.key}
    //         Old Value: ${event.oldValue}
    //         New Value: ${event.newValue}
    //         URL: ${event.url}
    //         ------------------------------
    //         `;
    //     nativeLog.innerHTML += logEntry;
    //     nativeLog.scrollTop = nativeLog.scrollHeight;
    // });

    // 设置localStorage
    setBtn.addEventListener('click', () => {
        const key = 'testKey';
        const value = `Value-${Date.now()}`;
        const oldValue = localStorage.getItem(key);

        localStorage.setItem(key, value);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            设置了: ${key} = ${value}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 移除localStorage
    removeBtn.addEventListener('click', () => {
        const key = 'testKey';
        const oldValue = localStorage.getItem(key);

        localStorage.removeItem(key);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            移除了: ${key}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 清空localStorage
    clearBtn.addEventListener('click', () => {
        localStorage.clear();

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            清空了所有localStorage数据
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });
</script>
</body>
</html>

在这里插入图片描述

七、CustomEvent+BroadcastChannel实现StorageEvent

通过代码分析可以发现,原生CustomEvent和MessageChannel机制无法实现跨页面监听storage变更,而BroadcastChannel虽然支持跨页面通信但在单页面内的多模块协同场景中存在局限性。为构建更完善的监听方案,可采用CustomEvent与BroadcastChannel相结合的方式:利用CustomEvent实现单页面内的高效精准监听,同时通过BroadcastChannel确保跨页面的实时同步,从而打造一个既能满足同域多页面数据同步需求,又能完美支持单页面多模块通信的增强型StorageEvent监听体系。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage同页面监听问题</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .box {
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
        }
        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        .log {
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f9f9f9;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>localStorage同页面监听问题演示</h1>

    <div class="box">
        <h2>操作面板</h2>
        <button id="setBtn">设置localStorage</button>
        <button id="removeBtn">移除localStorage</button>
        <button id="clearBtn">清空localStorage</button>
    </div>

    <div class="box">
        <h2>原生storage事件监听</h2>
        <p>这个区域显示原生storage事件的触发情况</p>
        <div id="nativeLog" class="log"></div>
    </div>

    <div class="box">
        <h2>同页面修改日志</h2>
        <p>这个区域显示当前页面对localStorage的直接操作</p>
        <div id="directLog" class="log"></div>
    </div>

    <div class="tips">
        <h3>问题说明:</h3>
        <p>1. 点击上方按钮修改localStorage</p>
        <p>2. 原生storage事件<strong>不会</strong>在同页面修改时触发</p>
        <p>3. 只有<strong>其他同源页面</strong>修改localStorage时才会触发</p>
        <p>4. 要测试原生事件,请打开另一个同源页面并修改localStorage</p>
    </div>
</div>

<script>
    // 获取DOM元素
    const setBtn = document.getElementById('setBtn');
    const removeBtn = document.getElementById('removeBtn');
    const clearBtn = document.getElementById('clearBtn');
    const nativeLog = document.getElementById('nativeLog');
    const directLog = document.getElementById('directLog');

    const handleStorageChange = (info) => {
        const logEntry = `
            [Proxy监听] ${new Date().toLocaleTimeString()}
            操作类型: ${info.type}
            Key: ${info.key}
            Old Value: ${info.oldValue}
            New Value: ${info.newValue}
            Message: ${info.message}
            ------------------------------
            `;
        nativeLog.innerHTML += logEntry;
        nativeLog.scrollTop = nativeLog.scrollHeight;
    }

    const createLocalStorageEvent = (key, oldValue, newValue, type, message) => {
        return new CustomEvent('localStorageChange',{
            detail:{
                key,
                oldValue,
                newValue,
                type,
                timestamp: Date.now(),
                url: window.location.href,
                message: message
            }
        });
    };

    window.addEventListener('localStorageChange', (event) => {
        handleStorageChange(event.detail);
    });

    // 创建 BroadcastChannel
    const storageChannel = new BroadcastChannel('local_storage_channel');
    const originalSetItem = localStorage.setItem;
    const originalRemoveItem = localStorage.removeItem;

    // 重写 setItem,在存储数据时触发自定义事件
    localStorage.setItem = function(key, value) {
        const oldValue = localStorage.getItem(key);
        originalSetItem.call(localStorage, key, value); // 调用原始方法
        window.dispatchEvent(createLocalStorageEvent(key, oldValue, value, 'setItem',`Key "${key}" 被修改: ${oldValue}${value}`)); // 手动触发事件
        // 通过 BroadcastChannel 广播消息
        storageChannel.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: value,
            action: 'set'
        });
    };

    // 重写 removeItem,在移除数据时触发自定义事件
    localStorage.removeItem = function(key) {
        const oldValue = localStorage.getItem(key);
        originalRemoveItem.call(localStorage, key); // 调用原始方法
        window.dispatchEvent(createLocalStorageEvent(key, oldValue, null, 'removeItem',`Key "${key}" 被移除 (旧值: ${oldValue})`)); // 手动触发事件

        storageChannel.postMessage({
            type: 'storage',
            key,
            oldValue,
            newValue: null,
            action: 'remove'
        });

    };

    // 监听 BroadcastChannel 消息
    storageChannel.onmessage = function(event) {
        const { type, key, oldValue, newValue, action } = event.data;

        if (type === 'storage') {
            let message = '';
            switch (action) {
                case 'set':
                    message = `Key "${key}" 被修改: ${oldValue}${newValue}`;
                    break;
                case 'remove':
                    message = `Key "${key}" 被移除 (旧值: ${oldValue})`;
                    break;
                case 'clear':
                    message = 'localStorage 已清空';
                    break;
            }

            handleStorageChange({
                ...event,
                message: message,
            });
        }
    };

    // 原生storage事件监听
    // window.addEventListener('storage', (event) => {
    //     const logEntry = `
    //         [原生事件] ${new Date().toLocaleTimeString()}
    //         Key: ${event.key}
    //         Old Value: ${event.oldValue}
    //         New Value: ${event.newValue}
    //         URL: ${event.url}
    //         ------------------------------
    //         `;
    //     nativeLog.innerHTML += logEntry;
    //     nativeLog.scrollTop = nativeLog.scrollHeight;
    // });

    // 设置localStorage
    setBtn.addEventListener('click', () => {
        const key = 'testKey';
        const value = `Value-${Date.now()}`;
        const oldValue = localStorage.getItem(key);

        localStorage.setItem(key, value);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            设置了: ${key} = ${value}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 移除localStorage
    removeBtn.addEventListener('click', () => {
        const key = 'testKey';
        const oldValue = localStorage.getItem(key);

        localStorage.removeItem(key);

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            移除了: ${key}
            旧值: ${oldValue}
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });

    // 清空localStorage
    clearBtn.addEventListener('click', () => {
        localStorage.clear();

        const logEntry = `
            [直接操作] ${new Date().toLocaleTimeString()}
            清空了所有localStorage数据
            ------------------------------
            `;
        directLog.innerHTML += logEntry;
        directLog.scrollTop = directLog.scrollHeight;
    });
</script>
</body>
</html>

在这里插入图片描述
在这里插入图片描述

八、总结

1.精确监听localStorage变化:在同一页面内需要精准监控存储变更时,推荐采用StorageEvent或CustomEvent方案,两者都具有简洁明了的实现方式。
2.系统扩展需求:当项目需要构建高度可定制的事件系统时,CustomEvent凭借其灵活的自定义能力成为首选,可以完美支持各种特殊操作需求。
3.模块间高频通信:对于页面内不同组件间需要持续、高效的数据交换场景,MessageChannel的点对点通信机制能提供最优的性能保障。
4.跨窗口协作:在多窗口或多标签页需要保持数据同步的工作场景下,BroadcastChannel的广播特性是最有效的解决方案。
5.跨页面自定义存储事件:若需要实现跨页面且可定制的存储监听系统,可采用 CustomEvent与BroadcastChannel结合 的方案——通过BroadcastChannel广播存储变更消息,各页面用CustomEvent触发本地监听,从而兼顾跨页面通信与灵活的事件处理能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

局外人LZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值