原文地址:https://javascript.plainenglish.io/5-vanilla-js-patterns-that-replace-entire-libraries-3d22445bfbc0
原文作者: Ignatius Sani

你是否面临这样的困扰?node_modules 文件夹臃肿到 300MB,打包产物体积高达 2MB,应用加载需要漫长的 8 秒。然而,你可能没有意识到,你所安装的依赖中,大约 80% 的功能其实用几十行原生 JavaScript 就能轻松实现。
下面这五种设计模式就是最好的证明。
在安装下一个 npm 包之前
我们并非全盘否定库的价值,也不鼓励你重复造每一个轮子。但在你习惯性地敲下 npm install 之前,不妨先问自己一个简单的问题:
“这个功能,我是否能用大约 50 行代码自己实现?”
很多时候,答案是肯定的。下面分享的这五种模式,是我在实践中用来保持项目精简、缩小打包体积、并加深对底层原理理解的方法。每一种都能替代一个流行的库,每一种都足够健壮可用于生产环境,而更重要的是,每一种都能让你收获比单纯“安装依赖”多得多的知识。
1. 状态管理(替代 Redux / Zustand)
库的解决方案:
// Redux: ~40KB
// 复杂的 setup、actions、reducers、middleware
npm install redux react-redux
原生 JavaScript 方案:
通过大约30行代码,我们可以实现一个基于观察者模式(Observer Pattern)的简易状态管理。
// 简单的 observer pattern:~30 行
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(updates) {
this.state = { ...this.state, ...updates };
this.listeners.forEach(listener => listener(this.state));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
// 使用示例
const store = new Store({ count: 0, user: null });
store.subscribe(state => {
console.log('State changed:', state);
});
store.setState({ count: 1 }); // 触发所有订阅者
在 React 中使用:
我们可以创建一个自定义 Hook,将原生 Store 与 React 的响应式系统连接起来。
import { useState, useEffect } from 'react';
function useStore(store) {
const [state, setState] = useState(store.getState());
useEffect(() => {
const unsubscribe = store.subscribe(newState => {
setState(newState);
});
return unsubscribe; // 组件卸载时清理订阅
}, [store]);
return state;
}
// 在组件中使用
function Counter() {
const state = useStore(store);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => store.setState({ count: state.count + 1 })}>
Increment
</button>
</div>
);
}
何时选择原生方案?
- 状态结构简单的单页应用。
- 状态变量少于 10 个。
- 你需要对状态更新逻辑拥有完全的控制权。
何时依然需要库?
- 大型复杂应用,状态间关系错综复杂。
- 需要时间旅行调试(time-travel debugging)等高级功能。
- 团队已经熟悉并约定使用 Redux 等成熟模式。
你能学到什么?
观察者模式、订阅/发布机制,以及 React 的 useState 为何需要触发重新渲染。这是所有状态管理库的底层基石。
2. 事件总线(替代 EventEmitter / Mitt)
库的解决方案:
// mitt: ~200 bytes(已经很小,但仍然是依赖)
// eventemitter3: ~7KB
npm install mitt
原生 JavaScript 方案:
一个功能完备的事件总线,大约20行代码就能实现。
// 自定义 event bus:~20 行
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, data) {
if (!this.events[event]) return;
// 复制一份事件回调数组,避免在迭代过程中被修改
[...this.events[event]].forEach(callback => {
callback(data);
});
}
once(event, callback) {
const wrapper = (data) => {
this.off(event, wrapper);
callback(data);
};
this.on(event, wrapper);
}
}
在 React 中使用:
import { useEffect } from 'react';
// 创建全局 event bus 实例
export const eventBus = new EventBus();
// 自定义 Hook 简化使用
function useEvent(event, callback) {
useEffect(() => {
eventBus.on(event, callback);
return () => eventBus.off(event, callback);
}, [event, callback]);
}
// 组件A:监听事件
function Notification() {
useEvent('notification:show', (message) => {
alert(message);
});
return <div>Notifications enabled</div>;
}
// 组件B:触发事件
function LoginButton() {
const handleLogin = () => {
eventBus.emit('user:login', { id: 1, name: 'John' });
eventBus.emit('notification:show', 'Welcome back!');
};
return <button onClick={handleLogin}>Login</button>;
}
何时选择原生方案?
- 用于跨组件通信,避免属性层层传递(prop drilling)。
- 实现模块间的解耦。
- 简单的发布/订阅场景。
- 用户行为埋点、分析数据上报。
何时依然需要库?
- 需要通配符事件匹配(如
*.click)。
- 需要事件优先级、执行顺序控制等高级特性。
- 需要更强大的调试工具。
你能学到什么?
发布/订阅模式、事件驱动架构以及跨组件通信的核心机制。这是现代前端应用中无处不在的基础模式。
3. DOM 操作(替代 jQuery)
库的解决方案:
// jQuery: ~90KB(压缩后)
// 曾经必不可少,现在更多是习惯使然
npm install jquery
原生 JavaScript 方案:
现代浏览器原生 API 已经非常强大,完全可以覆盖 jQuery 的常用功能。
// jQuery: $('.class').hide()
document.querySelectorAll('.class').forEach(el => el.style.display = 'none');
// jQuery: $('#id').addClass('active')
document.getElementById('id').classList.add('active');
// jQuery: $('.item').on('click', handler)
document.querySelectorAll('.item').forEach(el => {
el.addEventListener('click', handler);
});
// jQuery: $.ajax()
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data));
你甚至可以封装一组 Helper 函数,获得类似 jQuery 的链式体验:
// 常用操作的 helper
const $ = {
select: (selector) => document.querySelector(selector),
selectAll: (selector) => [...document.querySelectorAll(selector)],
create: (tag, props = {}) => {
const el = document.createElement(tag);
Object.assign(el, props);
return el;
},
on: (selector, event, handler) => {
document.querySelectorAll(selector).forEach(el => {
el.addEventListener(event, handler);
});
},
addClass: (selector, className) => {
document.querySelectorAll(selector).forEach(el => {
el.classList.add(className);
});
},
removeClass: (selector, className) => {
document.querySelectorAll(selector).forEach(el => {
el.classList.remove(className);
});
}
};
// 使用示例(类似 jQuery 风格)
$.addClass('.button', 'active');
$.on('.button', 'click', () => console.log('clicked'));
const newDiv = $.create('div', {
className: 'card',
textContent: 'Hello'
});
真实示例:不使用 jQuery 实现 Tabs 组件
function Tabs(containerSelector) {
const container = document.querySelector(containerSelector);
const tabs = [...container.querySelectorAll('[data-tab]')];
const panels = [...container.querySelectorAll('[data-panel]')];
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
panels.forEach(p => p.classList.remove('active'));
tab.classList.add('active');
const panelId = tab.dataset.tab;
const panel = container.querySelector(`[data-panel="${panelId}"]`);
panel.classList.add('active');
});
});
}
// 使用
Tabs('#my-tabs');
何时选择原生方案?
- 目标为现代浏览器(覆盖率95%以上)。
- 中小型项目。
- 对打包体积非常敏感。
- 你想扎实掌握 DOM 基础 API。
何时依然需要 jQuery?
- 需要支持 IE 11 及以下等老旧浏览器。
- 维护一个巨大的、历史遗留的 jQuery 项目。
- 团队对 jQuery 的熟悉程度远超原生 API。
坦白说,在新项目中,jQuery 的应用场景已经非常稀少了。
你能学到什么?
DOM 操作的本质、querySelector 的原理、事件传播机制。jQuery 只是早期浏览器 API 不统一、功能不足时的“语法糖”。
4. 客户端路由(替代 React Router)
库的解决方案:
// react-router-dom: ~70KB
// 功能强大,但对简单应用来说可能过于重量级
npm install react-router-dom
原生 JavaScript 方案:
基于浏览器 History API,我们可以用约40行代码实现一个基础但可用的 SPA 路由器。
// 基于 History API 的简单 router:~40 行
class Router {
constructor(routes) {
this.routes = routes;
this.currentPath = window.location.pathname;
// 监听浏览器前进/后退
window.addEventListener('popstate', () => {
this.navigate(window.location.pathname, false);
});
// 拦截具有特定属性的链接点击
document.addEventListener('click', (e) => {
if (e.target.matches('[data-link]')) {
e.preventDefault();
this.navigate(e.target.getAttribute('href'));
}
});
// 初始化渲染
this.navigate(this.currentPath, false);
}
navigate(path, pushState = true) {
this.currentPath = path;
if (pushState) {
window.history.pushState({}, '', path);
}
this.render();
}
render() {
const route = this.routes[this.currentPath] || this.routes['/404'];
const container = document.getElementById('app');
if (route) {
container.innerHTML = route();
}
}
}
// 使用示例
const routes = {
'/': () => `<h1>Home Page</h1>`,
'/about': () => `<h1>About Us</h1>`,
'/404': () => `<h1>Page Not Found</h1>`
};
new Router(routes);
在 React 中集成:
思路是类似的,将路由变化映射到 React 组件的渲染。
import { useState, useEffect } from 'react';
function useRouter(routes) {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
const handleLocationChange = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', handleLocationChange);
// 同样可以拦截 data-link 点击...
return () => window.removeEventListener('popstate', handleLocationChange);
}, []);
return routes[currentPath] || routes['/404'];
}
// 在组件中使用
function App() {
const PageComponent = useRouter({
'/': HomePage,
'/about': AboutPage
});
return <PageComponent />;
}
你能学到什么?
History API 的工作原理、单页应用如何拦截和阻止默认导航、路由匹配算法、以及避免整页刷新的机制。这是所有前端路由库共同构建的基础。
5. HTTP 请求(替代 Axios)
库的解决方案:
// axios: ~13KB
// API 设计友好,但现代 fetch API 已能覆盖 90% 的场景
npm install axios
原生 JavaScript 方案:
现代浏览器内置的 fetch API 功能强大,只需简单封装即可满足大部分需求。
// 一个增强的 fetch 包装器
const http = {
async get(url, options = {}) {
const response = await fetch(url, { ...options, method: 'GET' });
return this._handleResponse(response);
},
async post(url, data, options = {}) {
const response = await fetch(url, {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options.headers },
body: JSON.stringify(data)
});
return this._handleResponse(response);
},
// 类似地实现 put, patch, delete...
async _handleResponse(response) {
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
try {
error.data = await response.json();
} catch {
error.data = await response.text();
}
throw error;
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json();
}
return response.text();
}
};
// 使用示例
try {
const data = await http.get('/api/users');
console.log(data);
} catch (error) {
console.error('Request failed:', error.status, error.data);
}
实现请求拦截器:
// 简单的拦截器机制
const httpWithInterceptors = (() => {
const interceptors = { request: [], response: [] };
async function request(url, config) {
// 执行请求拦截器
let finalConfig = config;
for (const interceptor of interceptors.request) {
finalConfig = await interceptor(finalConfig);
}
let response = await fetch(url, finalConfig);
// 执行响应拦截器
for (const interceptor of interceptors.response) {
response = await interceptor(response);
}
return response;
}
return {
get: (url, config) => request(url, { ...config, method: 'GET' }),
post: (url, data, config) => request(url, { ...config, method: 'POST', body: JSON.stringify(data) }),
interceptors
};
})();
// 添加一个请求拦截器(如添加认证头)
httpWithInterceptors.interceptors.request.push((config) => {
config.headers = { ...config.headers, Authorization: `Bearer ${token}` };
return config;
});
在 React 中使用:自定义 Hook
import { useState, useCallback } from 'react';
function useHttp() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = useCallback(async (url, options = {}) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json();
return data;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, []);
return { sendRequest, loading, error };
}
你能学到什么?
Fetch API 的详细用法、HTTP 请求/响应模型、如何优雅地处理错误、以及使用 AbortController 取消请求。这是构建任何 Web 应用都必须掌握的基础能力。
依赖的真实成本
打包体积(Bundle Size)
上述 5 个库(Redux, Mitt/Axios, jQuery, React Router, Axios)加起来,轻易就能让你的应用打包体积增加数百 KB。这意味着用户需要多等待几秒才能看到内容。在移动端网络环境下,这几秒的差距可能就是用户留存与跳出的分界线。
维护负担(Maintenance Burden)
每一个外部依赖,都是一份长期承诺。你需要关心它的安全更新、破坏性变更、API 废弃通知。你安装的不仅仅是代码,更是持续的维护成本和潜在的升级风险。
理解鸿沟(Understanding Gap)
当依赖的库出现问题时,你很可能无法直接修复。你只能去翻看 GitHub Issues,等待维护者的回复,或者寻找蹩脚的绕过方案。因为你从未了解过它的内部工作机制。
但如果这是你自己写的代码呢?你理解每一行逻辑,遇到 Bug 可能在几分钟内就能定位并修复。你不必等待他人的 Pull Request,也不必依赖任何人的排期。
核心原则:不是“不用库”,而是“有意识地用库”
应该使用库的情况:
- 问题域本身极其复杂(例如日期时间处理、国际化 i18n)。
- 边界情况处理至关重要(例如安全性、可访问性)。
- 库经过充分验证和广泛使用(例如 React 本身,而不是某个随机的小众状态管理库)。
- 你的开发时间比节省下来的那点打包体积宝贵得多。
- 自己实现等价于重新发明一个复杂且易错的轮子。
可以不使用库的情况:
- 核心功能大约 50 行代码就能清晰实现。
- 你只用到该库 10% 的功能,却引入了 100% 的代码。
- 该库缺乏活跃维护,存在安全或兼容性风险。
- 选用它仅仅因为“大家都在用”,而非经过客观评估。
- 自己实现它能让你显著加深对某个重要技术概念的理解,从而在前端工程化的道路上变得更强大。
这些模式背后的共同理念
仔细观察,这五种模式共享着一些经典的软件设计思想:
- 观察者模式:这是状态管理和事件总线的共同核心。
- 封装:用简洁友好的 API 隐藏内部的复杂性。
- 函数核心:将核心逻辑设计为纯函数,而将副作用(如 DOM 操作、网络请求)控制在边缘。
- 可组合性:构建小而专注的模块,让它们能够像乐高积木一样自由组合。
这不仅仅是几个“原生 JavaScript 小技巧”,它们体现了编程中通用的、基础的设计模式。当你深刻理解了这些模式,所有基于它们构建的上层库在你眼中都将变得一目了然。
从小处开始实践
你不需要立刻推翻现有项目,全部重写。一个很好的起点是:下次当你准备 npm install 时,暂停一下,认真思考:
- 这个功能我能否用约 50 行代码实现?
- 自己实现是否能带来有价值的学习收获?
- 我是否真的需要这个库提供的全部功能?
从一个简单的模式开始尝试:亲手写一个事件总线,实现一个基础的路由器,或者封装一个带拦截器的 Fetch 工具函数。亲自感受一下实现的整个过程,评估你从中获得了哪些新知,并享受打包体积显著减少带来的成就感。
然后,基于这些真实的体验,再做出明智的决定:用库,还是不用。无论如何,这都将是一个经过深思熟虑的、主动的技术决策,而非盲目跟风的默认行为。
而这,正是一位“熟练使用工具的开发者”与一位“深刻理解工具本质的开发者”之间最根本的区别。在 云栈社区 与更多开发者交流,能帮助你更好地进行这类技术决策与实践。