一个“全局变量”引发的线上事故

你正在给一家 SaaS 客户做「企业级仪表盘」项目。

需求很简单:把 20 多个子系统的实时指标汇总到一张大屏。

为了"图省事",你在入口文件里顺手写了这么一段:

js 复制代码
// main.js
window.dashboard = {};                 // 🔍 全局缓存
window.dashboard.cache = new Map();    // 🔍 存接口数据
window.dashboard.refresh = () => {     // 🔍 供子系统调用
  fetch('/api/metrics').then(r => r.json())
    .then(data => window.dashboard.cache = new Map(data));
};

上线两周后,客户反馈:

"切到别的标签页再回来,大屏偶尔会白屏,刷新又好了。"

排查发现,Chrome 在内存紧张时会 回收后台标签页的 JS 堆 ,但 window 对象属于宿主环境,不会被回收

结果:

  • 旧的 dashboard 引用还在,但闭包里的 Map 被 GC 清掉,变成空壳;
  • 子系统继续调用 window.dashboard.refresh,拿到的永远是空数据;
  • 页面逻辑崩溃,白屏。

解决方案:三层防护,把风险降到 0

1. 表面用法:用 WeakMap 做"软引用"

js 复制代码
// 把全局缓存改成弱引用
const cache = new WeakMap();           // 🔍 不阻止 GC
window.dashboard = {
  get data() { return cache.get(document); }, // 🔍 与当前文档生命周期绑定
  set data(v) { cache.set(document, v); }
};

这样当用户切走标签页、文档被卸载时,cache 自动释放,切回来重新初始化,白屏消失。

2. 底层机制:为什么 window 不会被 GC?

层级 角色 生命周期 是否可被 GC
JS 堆 闭包、普通对象 无引用即回收
Host 环境 window 与标签页同生同灭
渲染进程 GPU 纹理、DOM 标签页关闭后统一清理

结论:window 是宿主对象,生命周期长于 JS 堆,挂上去的东西如果强引用 JS 对象,会导致"僵尸引用"。

3. 设计哲学:把"全局"变成"注入"

与其在 window 上挂变量,不如用 依赖注入 把作用域限制在模块内部:

js 复制代码
// dashboard.js
export const createDashboard = () => {
  const cache = new Map();             // 🔍 私有状态
  return {
    refresh: () => fetch('/api/metrics')
      .then(r => r.json())
      .then(data => cache.clear() && data.forEach(d => cache.set(d.id, d)))
  };
};
js 复制代码
// main.js
import { createDashboard } from './dashboard.js';
const dashboard = createDashboard();   // 🔍 生命周期由模块控制

应用扩展:可复用的配置片段

如果你的项目必须暴露全局 API(比如给第三方脚本调用),可以用 命名空间 + Symbol 双保险:

js 复制代码
// global-api.js
const NAMESPACE = Symbol.for('__corp_dashboard__');
window[NAMESPACE] = {                  // 🔍 避免命名冲突
  version: '2.1.0',
  createDashboard
};

环境适配说明:

  • 浏览器:Symbol 在 IE11 需 polyfill;
  • Node:同构渲染时 window 不存在,用 globalThis[NAMESPACE] 兜底。

举一反三:3 个变体场景

  1. 微前端基座
    window.__MICRO_APP_STORE__ 传递状态,但内部用 Proxy 做读写拦截,防止子应用直接篡改。
  2. Chrome 插件 content-script
    通过 window.postMessage 与页面通信,避免污染宿主全局。
  3. Electron 主进程/渲染进程
    contextBridge.exposeInMainWorld('api', safeApi) 把 API 注入到隔离上下文,防止 Node 权限泄露。

小结:

window 当"公告栏"可以,但别把"保险箱"也挂上去。

用弱引用、模块化和依赖注入,才能把全局变量的风险真正关进笼子里。

相关推荐
Amodoro13 分钟前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin21 分钟前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说1 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4531 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2431 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
三口吃掉你1 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself2431 小时前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴2 小时前
Tile Pattern
前端·webgl
前端工作日常2 小时前
前端基建的幸存者偏差
前端·vue.js·前端框架
Electrolux2 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法