找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1972

积分

0

好友

282

主题
发表于 昨天 02:14 | 查看: 5| 回复: 0

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

原文作者: Ignatius Sani

Vanilla JavaScript与库文件体积对比图

你是否面临这样的困扰?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 工具函数。亲自感受一下实现的整个过程,评估你从中获得了哪些新知,并享受打包体积显著减少带来的成就感。

然后,基于这些真实的体验,再做出明智的决定:用库,还是不用。无论如何,这都将是一个经过深思熟虑的、主动的技术决策,而非盲目跟风的默认行为。

而这,正是一位“熟练使用工具的开发者”与一位“深刻理解工具本质的开发者”之间最根本的区别。在 云栈社区 与更多开发者交流,能帮助你更好地进行这类技术决策与实践。




上一篇:深入剖析AtomicInteger底层实现:Java并发编程中的CAS无锁算法详解
下一篇:iOS 26普及率遇冷?仅15%用户升级,详解背后原因与视觉设计争议
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 13:58 , Processed in 0.202006 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表