普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月5日首页

Webpack的Loader和Plugin:你以为只是配置?其实是“流水线工人”和“包工头”

作者 kyriewen
2026年4月5日 13:47

你配置过Webpack吗?是不是照着文档写了几行rulesplugins,然后它就神奇地把代码打包好了?今天我们不背配置,直接钻进Webpack的肚子里,看看Loader和Plugin到底在干什么。看完你就能自己写一个Loader和一个Plugin,再也不用怕面试官问“Webpack原理”了。

前言

把Webpack想象成一家汽车工厂。原料是各种文件(JS、CSS、图片、字体……),产品是打包后的bundle。

  • Loader:流水线上的工人。每个工人只干一件事:把某种原料加工成下一个工人能处理的形式。比如把Sass转成CSS,把ES6转成ES5。
  • Plugin包工头。包工头不管具体加工,而是监听整个生产流程——开工前、完成某个环节后、打包结束——然后在合适的时机做全局性的事,比如抽离CSS、生成HTML、压缩代码。

今天我们就来认识这两位“功臣”,顺便自己动手写一个。

一、Loader:干啥啥都行,专精第一名

Loader是一个函数,它接收源文件内容,返回处理后的内容。一个文件可以经过多个Loader串联(从右到左,从下到上)。

// 一个最简单的Loader:把内容里的“Hello”换成“Hi”
module.exports = function(source) {
  const result = source.replace(/Hello/g, 'Hi');
  return result;
};

配置里Loader的执行顺序

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
        // 执行顺序:sass-loader → css-loader → style-loader
      }
    ]
  }
};

像流水线:sass-loader把SCSS转成CSS → css-loader处理CSS中的依赖(@import等)→ style-loader把CSS注入到DOM的<style>标签。

常用Loader举例

  • babel-loader:把ES6+转成ES5
  • css-loader:解析@importurl()
  • style-loader:把CSS插入DOM
  • file-loader:把文件输出到目录,返回路径
  • url-loader:小文件转成base64,大文件走file-loader
  • sass-loader:编译Sass/SCSS

动手写一个“清除console”的Loader

// clean-console-loader.js
module.exports = function(source) {
  // 移除console.log、console.warn等
  const cleaned = source.replace(/console\.(log|warn|error)\([^)]*\);?/g, '');
  return cleaned;
};

在webpack.config.js里使用:

module: {
  rules: [
    {
      test: /\.js$/,
      use: path.resolve(__dirname, 'clean-console-loader.js')
    }
  ]
}

二、Plugin:包工头,管全局

Plugin是一个(或者带有apply方法的对象)。它通过监听Webpack生命周期里的钩子(hooks),在特定时机干预打包过程。

class MyPlugin {
  apply(compiler) {
    // 在打包结束后执行
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('🎉 打包完成了!');
    });
  }
}

Webpack的钩子有同步和异步之分。比如emit(生成资源到输出目录之前)是异步的,要用tapAsync

常用Plugin举例

  • HtmlWebpackPlugin:自动生成HTML,并注入打包后的JS/CSS
  • MiniCssExtractPlugin:把CSS抽成单独文件
  • DefinePlugin:定义全局常量(比如环境变量)
  • CleanWebpackPlugin:打包前清理输出目录

动手写一个“打包完成发通知”的Plugin

class NotifyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('NotifyPlugin', (stats) => {
      const time = stats.endTime - stats.startTime;
      console.log(`✅ 打包成功,耗时 ${time}ms`);
      // 这里可以调系统通知API(需要额外库)
    });
  }
}
module.exports = NotifyPlugin;

使用:

const NotifyPlugin = require('./notify-plugin');
plugins: [new NotifyPlugin()]

三、Loader和Plugin的核心区别

维度 Loader Plugin
职责 转换单个文件 影响整个构建流程
作用范围 匹配test正则的文件 全局
实现形式 函数 类(带apply方法)
运行时机 模块加载过程中 生命周期钩子
常见例子 babel-loader, css-loader HtmlWebpackPlugin, CleanPlugin

形象比喻:

  • Loader:工人,只会加工原料。
  • Plugin:包工头,指挥全局,监听事件。

四、编写Loader的进阶技巧

1. 获取Loader选项

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  const options = loaderUtils.getOptions(this);
  // 使用options...
  return source;
};

2. 异步Loader

如果Loader里要异步操作(比如网络请求),用this.async()

module.exports = function(source) {
  const callback = this.async();
  setTimeout(() => {
    const result = source.toUpperCase();
    callback(null, result);
  }, 1000);
};

3. 生成多个文件

可以用this.emitFile

const { RawSource } = require('webpack-sources');
module.exports = function(source) {
  this.emitFile('new-file.txt', new RawSource('hello'));
  return source;
};

五、编写Plugin的进阶技巧

1. 常用钩子

  • compiler.hooks.entryOption:读取入口配置后
  • compiler.hooks.beforeRun:开始编译前
  • compiler.hooks.compile:编译前
  • compiler.hooks.emit:生成资源到输出目录前(可以修改文件内容)
  • compiler.hooks.done:打包完成

2. 修改输出内容

class ModifyFilePlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('ModifyFilePlugin', (compilation, callback) => {
      // compilation.assets 包含所有待输出文件
      const content = compilation.assets['bundle.js'].source();
      const modified = content.replace('old', 'new');
      compilation.assets['bundle.js'] = {
        source: () => modified,
        size: () => modified.length
      };
      callback();
    });
  }
}

六、总结:从配置使用者到原理掌握者

  • Loader:文件转换器,函数式,串联处理。
  • Plugin:构建流程干预者,事件监听式,做全局工作。
  • 掌握原理后,你就能:
    • 写自定义Loader处理特殊文件(比如把XML转成JS对象)
    • 写自定义Plugin做自动上传CDN、生成资源清单等
    • 更从容地调试Webpack配置错误

下次面试官问“Webpack的Loader和Plugin区别”,你可以自信地画出流水线和包工头的比喻,顺便掏出自己写的Loader和Plugin代码。

如果你觉得今天的“工厂之旅”够形象,点个赞让更多人看到。明天我们将深入Webpack优化——如何让打包速度飞起,让产物体积瘦成闪电。我们明天见!

昨天以前首页

你点的“刷新”是假刷新?前端路由的瞒天过海术

作者 kyriewen
2026年4月4日 11:32

为什么单页应用切换页面时,浏览器没有真正刷新?地址栏变了,页面却没白一下?今天我们来拆穿前端路由的“魔术”——它根本没去服务器要新页面,而是自己偷偷换了内容。看完这篇,你也能实现一个自己的前端路由。

前言

你有没有注意过,现在很多网站(比如知乎、B站、Github)点开一个新页面,地址栏变了,但页面没有那种“白屏-加载-闪现”的过程,而是瞬间切换内容。这就像你走进一家餐厅,菜单上写着“换桌”,你以为换了个房间,结果服务员只是把你桌上的桌布换了。

这就是前端路由干的“好事”。它让页面看起来跳转了,实际上只是JS在背后偷偷换了DOM,地址栏的变化也是骗你的。今天我们就来揭开这个魔术的奥秘,顺便自己写一个简单的路由。

一、什么是前端路由?

传统网站,点击链接会向服务器请求一个新HTML,浏览器刷新整个页面。这叫后端路由

单页应用(SPA)里,所有页面逻辑都在一个HTML里。切换“页面”时,不会请求新HTML,而是JS擦掉旧内容,画上新内容。同时,通过某种手段改变浏览器的地址栏URL,让用户感觉像换了个页面。这就是前端路由

前端路由的实现依赖两个“戏法”:

  • 改变URL但不刷新页面
  • 监听URL变化并渲染对应组件

二、Hash模式:带#号的“假跳转”

早期前端路由用的是hash(也就是URL里#后面的部分)。改变#后的值,不会触发页面刷新,也不会向服务器发请求。浏览器自己会记录历史(前进后退可用)。

// 改变hash
window.location.hash = 'home';

// 监听hash变化
window.addEventListener('hashchange', () => {
  const hash = window.location.hash.slice(1); // 去掉#
  renderPage(hash);
});

比如https://example.com/#/home,你改成#/about,页面不会刷新,但hashchange事件会触发,你可以在回调里根据hash渲染不同内容。

优点:兼容性好,IE也能用。
缺点:URL有个丑陋的#;服务端无法捕获#后面的内容(因为#之后的部分不会发到服务器)。

三、History模式:看起来像真的

HTML5新增了pushStatereplaceState,可以改变URL路径,同样不刷新页面。加上popstate事件监听,就能实现干净的路由(没有#)。

// 改变URL(添加一条历史记录)
history.pushState({ page: 'home' }, 'Home', '/home');

// 替换当前历史记录(不新增)
history.replaceState({ page: 'about' }, 'About', '/about');

// 监听前进后退
window.addEventListener('popstate', (event) => {
  const state = event.state; // pushState时传的数据
  renderPage(location.pathname);
});

优点:URL干净,像真实多页面。
缺点:需要服务端配合——因为刷新页面时,浏览器会按真实路径请求服务器,如果服务器没配置,会404。解决方案:所有路由都返回同一个HTML(即index.html)。

四、手写一个迷你前端路由

我们来实现一个最简单的Hash路由,包含三个“页面”:首页、关于、404。

<nav>
  <a href="#/home">首页</a>
  <a href="#/about">关于</a>
  <a href="#/nothing">不存在</a>
</nav>
<div id="app">内容会变</div>
function renderPage(path) {
  const app = document.getElementById('app');
  if (path === '/home') {
    app.innerHTML = '<h2>🏠 首页</h2><p>欢迎来到我的网站</p>';
  } else if (path === '/about') {
    app.innerHTML = '<h2>📖 关于</h2><p>这是一个前端路由演示</p>';
  } else {
    app.innerHTML = '<h2>❌ 404</h2><p>页面不存在</p>';
  }
}

// 监听hash变化
window.addEventListener('hashchange', () => {
  const hash = window.location.hash.slice(1); // 去掉#
  renderPage(hash || '/home');
});

// 页面加载时执行一次
window.addEventListener('load', () => {
  const hash = window.location.hash.slice(1);
  renderPage(hash || '/home');
});

就这么几行,你已经实现了一个前端路由。当然,实际框架里的路由更复杂(嵌套路由、动态参数、路由守卫等),但核心原理就是监听URL变化 + 渲染对应组件。

五、前端路由与后端路由的区别

特性 后端路由 前端路由
请求方式 每次跳转都请求服务器 不请求服务器(JS切换内容)
刷新页面 会重新下载HTML 会刷新但需要服务端配合(history模式)
首屏加载 只加载当前页面 通常要加载所有JS(可代码分割)
用户体验 有白屏、闪烁 切换流畅
SEO 友好 较差(需SSR或预渲染)

六、常见坑点与解决方案

1. History模式刷新404

配置Nginx将所有路由指向index.html:

location / {
  try_files $uri $uri/ /index.html;
}

2. 路由跳转但页面不滚动

单页切换时,滚动条位置可能保留在上一个页面的位置。需要在路由变化后手动window.scrollTo(0, 0)

3. 动态路由参数

比如/user/:id,你需要从路径中提取id。可以用正则或简单分割:

function matchRoute(path, routePath) {
  const pathParts = path.split('/');
  const routeParts = routePath.split('/');
  if (pathParts.length !== routeParts.length) return null;
  const params = {};
  for (let i = 0; i < pathParts.length; i++) {
    if (routeParts[i].startsWith(':')) {
      params[routeParts[i].slice(1)] = pathParts[i];
    } else if (routeParts[i] !== pathParts[i]) {
      return null;
    }
  }
  return params;
}

七、总结

  • 前端路由让单页应用切换页面时不刷新,体验流畅。
  • Hash模式# + hashchange,兼容性好,但URL丑。
  • History模式pushState + popstate,URL干净,需服务端配合。
  • 原理很简单:监听URL变化 → 根据路径渲染不同内容。
  • 现代框架(React Router、Vue Router)都是在此基础上增强。

下次再看到地址栏变了但页面没白,你就可以自信地说:“哼,不过是在演我。”

如果你喜欢今天的“魔术揭秘”,点个赞让更多人看到。明天我们将聊聊Webpack的Loader和Plugin原理,从零理解构建工具的核心。我们明天见!

本地存储全家桶:从localStorage到IndexedDB,把数据塞进用户浏览器

作者 kyriewen
2026年4月3日 20:36

你有没有想过,为什么刷新页面后,有些网站还能记住你的登录状态?为什么购物车里的商品关掉浏览器再打开还在?今天我们就来聊聊浏览器里的“记忆术”——本地存储。从简单的钥匙串localStorage,到能装下整个图书馆的IndexedDB,总有一款适合你。

前言

想象一下,你每次去网吧上网,都要重新登录所有账号、重新设置主题、重新添加购物车——是不是想砸电脑?还好,浏览器有“记忆功能”。它能在你的电脑里存点东西,下次再来时直接拿出来用。

这个“记忆功能”就是Web存储。今天我们就来盘点一下浏览器提供的几种存储方式:localStorage、sessionStorage、cookie,以及能存视频、存大文件的IndexedDB。看完你就能根据场景选对工具,再也不用担心数据“蒸发”了。

一、localStorage:永不过期的便利贴

localStorage是一个挂在window上的对象,它存的数据没有过期时间,除非你手动清除或者用户清理浏览器缓存,否则会一直待在那里。

基本用法

// 存数据(键值对,值必须是字符串)
localStorage.setItem('username', '张三');
localStorage.setItem('theme', 'dark');

// 取数据
const name = localStorage.getItem('username'); // '张三'

// 删除某条
localStorage.removeItem('theme');

// 全部清空
localStorage.clear();

// 获取存储数量
console.log(localStorage.length);

存对象怎么办?

localStorage只能存字符串,所以对象要先转成JSON:

const user = { name: '张三', age: 18 };
localStorage.setItem('user', JSON.stringify(user));

// 读取时解析
const stored = JSON.parse(localStorage.getItem('user'));

容量限制

大多数浏览器限制5MB~10MB,够存一些配置、用户信息、小量缓存。

特点总结

  • 同步:操作是同步的,会阻塞主线程(但一般很快)。
  • 同源:同一域名下所有页面共享(包括不同标签页)。
  • 永久:除非手动清除。
  • 仅客户端:不会自动发送到服务器。

二、sessionStorage:标签页关闭就消失的临时工

sessionStoragelocalStorage的API一模一样,但生命周期不同:它只存在于当前标签页。关掉标签页,数据就没了。刷新页面还在,但新开标签页(即使是同一个网站)会得到一个新的sessionStorage。

// 用法完全一样
sessionStorage.setItem('tempData', '临时值');

适用场景:表单临时草稿、当前页面的中间状态、不希望跨页面共享的敏感信息。

三、cookie:老前辈,但有点“重”

cookie是最早的浏览器存储机制,但如今除了会话管理(登录态)和少量用户追踪,大部分场景已被localStorage替代。

特点

  • 容量小:每个cookie 4KB 左右。
  • 自动携带:每次HTTP请求都会把cookie发给服务器(增加带宽消耗)。
  • 可设置过期时间。
  • 可标记HttpOnly(禁止JS读取,防XSS)、Secure(仅HTTPS)、SameSite(防CSRF)。
// 设置cookie(繁琐)
document.cookie = "username=张三; expires=Thu, 18 Dec 2026 12:00:00 UTC; path=/";

// 读取cookie(需要自己解析)
console.log(document.cookie);

现在主流做法:用localStorage存非敏感数据,用httpOnly cookie存登录凭证

四、IndexedDB:浏览器里的“小数据库”

如果你要存的东西很大(几百MB),或者需要复杂的查询、索引、事务,那么localStorage就不够用了。这时候请出IndexedDB——一个运行在浏览器里的非关系型数据库。

特点

  • 容量大:通常250MB+,甚至更多(取决于浏览器)。
  • 异步API:基于Promise或回调,不阻塞主线程。
  • 支持索引、游标、事务。
  • 可以存储File、Blob、ArrayBuffer等二进制数据。

快速上手

IndexedDB的API比较原始,不过我们可以封装一下。

// 1. 打开/创建数据库
const request = indexedDB.open('MyDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建一个对象仓库(类似表),指定主键
  const store = db.createObjectStore('users', { keyPath: 'id' });
  // 创建索引,用于快速查询
  store.createIndex('name', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('数据库打开成功');
  // 后续增删改查都用这个db对象
};

request.onerror = (event) => {
  console.error('数据库打开失败', event.target.error);
};

增删改查

// 添加数据(在onsuccess里拿到db)
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const addRequest = store.add({ id: 1, name: '张三', age: 18 });

addRequest.onsuccess = () => console.log('添加成功');
addRequest.onerror = (e) => console.error('添加失败', e.target.error);

// 查询
const getRequest = store.get(1);
getRequest.onsuccess = () => console.log(getRequest.result);

// 更新(使用put,如果存在则覆盖)
store.put({ id: 1, name: '李四', age: 20 });

// 删除
store.delete(1);

使用游标遍历

const range = IDBKeyRange.bound(1, 10); // id从1到10
store.openCursor(range).onsuccess = (e) => {
  const cursor = e.target.result;
  if (cursor) {
    console.log(cursor.value);
    cursor.continue(); // 继续下一个
  }
};

现代封装:localForage

原生IndexedDB API太啰嗦,推荐用localForage这个库,它提供了类似localStorage的简洁API,但背后自动选择IndexedDB、WebSQL或localStorage。

// 使用localForage
import localforage from 'localforage';

await localforage.setItem('user', { name: '张三' });
const user = await localforage.getItem('user');

五、四种存储方式对比

特性 localStorage sessionStorage cookie IndexedDB
容量 5-10MB 5-10MB 4KB 几百MB
生命周期 永久 标签页关闭 可设置过期 永久
跨标签页
异步 同步 同步 同步 异步
自动发到服务器 是(每次请求)
数据类型 字符串 字符串 字符串 任意(结构化克隆)
查询能力 索引、游标

六、选型指南:到底用哪个?

  • 简单键值对,少量数据localStorage,比如用户偏好设置、主题、是否首次访问。
  • 临时数据,只在一个页面用sessionStorage,比如多步骤表单的暂存。
  • 登录凭证httpOnly cookie(安全)配合后端。
  • 大量结构化数据、离线应用IndexedDB,比如邮件客户端、笔记应用、缓存API数据。
  • 需要与后端自动同步cookieAuthorization头(用localStorage存token也行,但要注意XSS)。

七、避坑指南

1. localStorage 的同步阻塞

大量数据存取会阻塞UI,建议不要存超过几MB,或改用IndexedDB。

2. 隐私模式

Safari的隐私模式下,localStorage和IndexedDB可能不可用或容量极低,要写try-catch降级。

3. 序列化问题

localStorage存对象会丢失原型链、函数、Symbol、循环引用。用JSON.stringify前确保数据可序列化。

4. 安全提醒

永远不要把敏感信息(如密码、token)明文存在localStorage,因为任何JS都能读到(XSS攻击)。token建议用httpOnly cookie或短时效+refresh机制。

5. IndexedDB 版本升级

当修改数据库结构时,需要增加版本号,并在onupgradeneeded里处理旧数据迁移,否则会报错。

八、总结:存储就像选工具箱

  • localStorage:日常杂货,随手放。
  • sessionStorage:临时工,关窗走人。
  • cookie:老古董,特殊场合用。
  • IndexedDB:重武器,存大文件、复杂查询。

掌握了这些,你就可以在浏览器里随心所欲地存数据了。明天我们将继续前端工程化的旅程,聊聊Cookie与Session的区别,以及现代认证方案JWT。

如果你觉得今天的存储全家桶够实用,点个赞让更多人看到。我们明天见!

MutationObserver:DOM界的“卧底”,暗中观察每个风吹草动

作者 kyriewen
2026年4月2日 13:50

你想知道页面上的某个元素什么时候被偷偷改了吗?比如有个熊孩子脚本悄悄改了你的广告位,或者某个懒加载图片终于加载完了?今天我们就来请一位“卧底”——MutationObserver,让它24小时盯着DOM树,任何变化都逃不过它的眼睛。

前言

假设你开了一家便利店,店里装了监控。你想知道:什么时候有人进来?什么时候货架上的商品被拿走了?什么时候价格标签被换了?普通的监控只能录像,但你需要的是“智能警报”——一有变化就通知你。

这就是MutationObserver的活。它是浏览器提供的一个API,专门用来监听DOM树的变化:节点增删、属性修改、文本内容改变……统统能抓到。而且它不会像setInterval那样一直轮询,性能好得多。

一、MutationObserver是啥?

MutationObserver是一个构造函数,用来创建一个观察者对象。你可以给它指定一个回调函数,然后让它去“盯”某个DOM节点。一旦这个节点或它的子孙节点发生变化,回调函数就会被触发。

// 创建一个观察者实例,传入回调
const observer = new MutationObserver((mutationsList, observer) => {
  for (let mutation of mutationsList) {
    console.log(mutation.type, '发生了变化');
  }
});

// 指定要观察的节点
const targetNode = document.getElementById('watch-me');

// 开始观察
observer.observe(targetNode, {
  attributes: true,    // 观察属性变化
  childList: true,     // 观察子节点增删
  subtree: true,       // 观察所有后代节点
  characterData: true  // 观察文本内容变化
});

// 某天不想观察了
// observer.disconnect();

二、能观察到哪些变化?

配置选项决定了你关心哪些“风吹草动”:

  • attributes:属性变了(比如classstylesrc被改)
  • childList:子节点被增删(添加或删除元素、文本节点)
  • characterData:文本节点的内容变了
  • subtree:是否监听后代节点(默认false,只监听目标节点)
  • attributeFilter:只监听特定属性,比如['class', 'src']
  • attributeOldValue:是否记录旧属性值
  • characterDataOldValue:是否记录旧文本值

三、实战:监听广告位有没有被篡改

很多网站会在页面上放广告,但有些恶意脚本会偷偷把广告位换成自己的内容。用MutationObserver可以第一时间发现并报警。

<div id="ad-container">
  <img src="real-ad.jpg" alt="官方广告">
</div>
const adContainer = document.getElementById('ad-container');

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      // 子节点被改了
      console.warn('⚠️ 广告位内容被篡改!');
      // 可以上报服务器,或者恢复内容
    } else if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
      console.warn('⚠️ 广告图片被替换了!');
    }
  });
});

observer.observe(adContainer, {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ['src', 'href']
});

四、实战:监听输入框内容变化(代替input事件?)

input事件已经能监听输入框变化,但MutationObserver可以监听更底层的文本节点变化,比如通过JS直接修改.valueinput事件可能不触发,但MutationObserver可以。

<input id="username" type="text">
const input = document.getElementById('username');

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
      console.log('输入框的值被改了,新值:', input.value);
    }
  });
});

observer.observe(input, {
  attributes: true,
  attributeFilter: ['value']
});

注意:这种方式监听value属性变化,只对通过JS设置.value有效,用户手动输入不会触发(因为用户输入不改变value属性,而是改变元素的defaultValue和内部状态)。所以实际中监听输入框还是input事件更合适。这里只是演示能力。

五、实战:监听动态加载的图片,做懒加载

很多懒加载库用IntersectionObserver,但如果你想知道图片什么时候被添加到DOM,可以用MutationObserver。

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    mutation.addedNodes.forEach((node) => {
      if (node.nodeType === 1 && node.tagName === 'IMG') {
        console.log('新图片出现了:', node.src);
        // 可以在这里做懒加载初始化
      }
    });
  });
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

六、性能注意事项

MutationObserver虽然比轮询好,但也不能滥用。以下几点要注意:

  1. 不要观察整个document:如果你observe(document.body, { subtree: true, childList: true, attributes: true }),那页面上的任何变化都会触发回调,频繁执行可能影响性能。尽量把观察范围缩小到具体容器。

  2. 回调里不要做太重的操作:MutationObserver的回调是在微任务中执行的,如果里面操作DOM或者计算太多,会阻塞后续渲染。

  3. 及时disconnect:如果不再需要观察,记得调用disconnect()释放资源。

  4. 使用takeRecords():在disconnect之前,可以调用observer.takeRecords()取出尚未处理的变化记录。

七、与旧API对比:Mutation Events的悲惨往事

很久以前,浏览器有一套Mutation Events(比如DOMNodeInsertedDOMAttrModified等)。它们的问题很多:

  • 性能差,每次变化都同步触发,容易导致重入和崩溃
  • 不支持批量观察
  • 被标记为废弃

MutationObserver是它们的完美替代,异步、批量、性能好。

八、总结:MutationObserver就是你的“鹰眼”

  • 它能监听DOM树的各种变化:属性、子节点、文本内容。
  • 配置灵活,可以精确到特定属性或是否包含后代。
  • 异步回调,批量返回变化记录,性能优秀。
  • 应用场景:监听动态内容加载、检测第三方脚本篡改、实现数据绑定(比如某些MVVM库的底层)、与React/Vue的虚拟DOM配合调试等。

有了MutationObserver,你就可以在DOM变化时第一时间响应,像一个隐形的守护者。明天我们将进入Web Storage的世界,看看localStorage、sessionStorage和IndexedDB怎么帮你把数据存到用户浏览器里。

如果你觉得今天的“卧底”够犀利,点个赞让更多人看到。我们明天见!

❌
❌