只读 matches 不会自动更新,需用 addeventlistener('change') 监听变化并及时清理;应通过 class 或 data-theme 控制主题,优先使用 localstorage 手动设置值,并注意 ssr 和旧版 safari 兼容性。

为什么只读 matches 会导致主题“卡死”
很多人写完 window.matchMedia('(prefers-color-scheme: dark)').matches 发现首屏是对的,但系统切深色后页面毫无反应——这不是 bug,是设计如此。matches 是一个只读布尔值,它只反映「调用那一刻」的系统状态,不会自动更新。你得靠事件机制才能捕获后续变化。
常见错误包括:
• 把 matches 判断写在初始化函数里就不管了
• 误以为 CSS 媒体查询生效了,JS 就不用监听(其实 JS 需要触发 DOM 更新、记录 localStorage、通知图表库等)
• 在 React 或 Vue 组件里监听却没清理,导致重复绑定、多次执行 handler
addEventListener('change', handler) 是唯一可靠写法
必须用 addEventListener,不是 onchange = handler,也不是已废弃的 addListener。后者在 Safari 14+ 和 Chrome 84+ 中完全失效,且不支持多监听器共存。
正确步骤:
• 先获取媒体查询对象:const mql = window.matchMedia('(prefers-color-scheme: dark)')
• 立即执行一次 handler,确保首屏主题正确
• 调用 mql.addEventListener('change', e => { /* 处理 e.matches */ })
• 在组件卸载或脚本退出前,务必调用 mql.removeEventListener('change', handler)
漏掉最后一步,轻则内存泄漏,重则刷新后监听器叠加,出现“切一次主题,样式变两次”的现象。
切换主题时别直接操作内联样式
直接写 element.style.backgroundColor = '#111' 看似快,但会和 CSS 变量、媒体查询、第三方库样式冲突,后期维护成本高。更健壮的做法是统一用 class 或 data-theme 控制作用域。
推荐方案:
• 用 document.documentElement.classList.toggle('dark', e.matches) 切换根元素类名
• CSS 里写 html.dark .card { background: var(--card-bg); }
• 所有颜色变量定义在 :root,再用 @media (prefers-color-scheme: dark) 覆盖
• 这样 JS 只管“通知”,CSS 负责“渲染”,解耦清晰,也兼容 SSR
手动设置优先级高于系统偏好,但不能覆盖失败
用户点了一次「深色模式」开关,你得记住;下次加载时,这个选择应优先于系统设置。但很多实现漏掉了“覆盖保护”逻辑:
• 先读 localStorage.getItem('theme'),如果值为 'dark' 或 'light',就按此设置,不再查 mql.matches
• 只有该值为 null 时,才 fallback 到系统偏好
• 监听 change 事件时,也要判断:仅当 localStorage 里没存过手动值,才自动同步系统变化
• 否则会出现“用户刚关掉深色模式,系统一切换又自动打开”的反直觉行为
最容易被忽略的是:SSR 环境下未加 typeof window !== 'undefined' 保护,服务端执行 window.matchMedia 直接报错;还有旧版 Safari(≤13.1)不支持 addEventListener,需回退到 addListener —— 这两点出问题时,现象往往是本地开发正常,上线后白屏或主题错乱。











