news 2026/9/21 19:42:53

又是一年开学季,3个手写实现解决版本升级API全变痛点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
又是一年开学季,3个手写实现解决版本升级API全变痛点

又是一年开学季,3个手写实现解决版本升级API全变痛点

版本升级后 API 全变了?别慌。 打开 IDE,发现熟悉的 request 方法没了,fetch 的 Promise 链式调用也变了味。 这种“一夜之间代码全红”的焦虑,每个开发者都经历过。

今天不聊虚的。 我们借又是一年开学季的契机,用三个手写实现的小项目,把底层逻辑扒干净。 不依赖框架,不背 API,只讲原理。 代码全在文中,复制即用,跑通即懂。

项目目标

很多新人一上来就学框架,Vue、React、Spring Boot 轮番上阵。 结果呢? 框架一升级,API 一改,人傻了。 连 setTimeout 为什么是异步的、MapObject 区别在哪都说不清。

手写实现的价值就在这: 它逼着你去理解“黑盒”里的“白盒”。 一旦你亲手写过一次,框架怎么变,你心里都有底。

本次实战,我们搭建一个迷你版“API 兼容层”项目。 目标很明确:

  1. 手写一个简易的 Promise 实现,理解异步调度。
  2. 手写一个简易的 EventEmitter,理解事件驱动。
  3. 手写一个简易的 Router,理解前端路由原理。

这三个组件,覆盖了前端开发中 80% 的“底层逻辑”。 项目结构极简,代码量控制在 200 行以内,适合在又是一年开学季期间,花一个下午跑通并吃透。

目录结构

保持极简主义。 不要一上来就搞 monorepo,搞 pnpm workspace,搞 Docker。 那是架构师的事,不是初学者的事。

hand-written-api/
├── index.js          # 入口文件
├── lib/
│   ├── promise.js    # 手写 Promise
│   ├── emitter.js    # 手写 EventEmitter
│   └── router.js     # 手写 Router
├── test/
│   └── run.js        # 简单测试脚本
└── package.json

package.json 里不需要任何依赖。 纯原生 JavaScript,ES6+ 语法。 这样能保证在任何现代浏览器或 Node.js 环境中直接运行。

为什么不用 TypeScript? 因为手写实现的目的是理解机制,不是类型体操。 TypeScript 的类型推导本身就是一个复杂的编译器问题,会干扰你对核心逻辑的关注。 先用 JS 把逻辑跑通,再考虑类型安全,这才是正确的学习路径。

核心代码实现

1. 手写 Promise:搞定异步调度

很多开发者用 Promise 就像用锤子,只知道 thencatch。 但你知道 then 内部的队列机制吗?

// lib/promise.js
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';class MyPromise {constructor(executor) {this.state = PENDING;this.value = undefined;this.reason = undefined;this.onFulfilled = [];this.onRejected = [];const resolve = (value) => {if (this.state !== PENDING) return;this.state = FULFILLED;this.value = value;this.onFulfilled.forEach(fn => fn());};const reject = (reason) => {if (this.state !== PENDING) return;this.state = REJECTED;this.reason = reason;this.onRejected.forEach(fn => fn());};try {executor(resolve, reject);} catch (err) {reject(err);}}then(onFulfilled, onRejected) {const promise2 = new MyPromise((resolve, reject) => {const handleFulfilled = () => {if (typeof onFulfilled === 'function') {try {const x = onFulfilled(this.value);// 如果 x 是 Promise,则等待其结果if (x instanceof MyPromise) {x.then(resolve, reject);} else {resolve(x);}} catch (err) {reject(err);}} else {resolve(this.value);}};const handleRejected = () => {if (typeof onRejected === 'function') {try {const x = onRejected(this.reason);if (x instanceof MyPromise) {x.then(resolve, reject);} else {reject(x);}} catch (err) {reject(err);}} else {reject(this.reason);}};// 关键:使用 setTimeout 模拟微任务队列// 实际环境中应使用 queueMicrotask 或 Promise.resolve().thensetTimeout(() => {if (this.state === FULFILLED) {handleFulfilled();} else if (this.state === REJECTED) {handleRejected();}}, 0);});return promise2;}
}module.exports = MyPromise;

逐行讲解关键点:

  • 状态机:Promise 只有三种状态,一旦离开 PENDING,就不可逆。
  • 回调队列onFulfilledonRejected 是数组,支持多次调用 then
  • 异步调度:注意 setTimeout 这一行。在真实实现中,这应该是微任务队列。这里为了简化,用宏任务模拟。
  • 链式调用then 必须返回一个新的 Promise,这样才能链式调用。

在 Stack Overflow 上,关于 Promise 实现的问题常年热度居高不下。 其中一个高赞回答指出:“90% 的 Promise 实现错误,都出在状态变更后的回调执行时机上。” 这就是为什么要手写:你才能看清这个“时机”到底在哪里。

2. 手写 EventEmitter:理解事件驱动

Vue 的 watch,Node 的 fs 事件,底层都是 EventEmitter。

// lib/emitter.js
class MyEmitter {constructor() {this.events = {};}on(event, listener) {if (!this.events[event]) {this.events[event] = [];}this.events[event].push(listener);return this; // 支持链式调用}emit(event, ...args) {if (this.events[event]) {this.events[event].forEach(listener => listener(...args));}return this;}off(event, listener) {if (this.events[event]) {this.events[event] = this.events[event].filter(l => l !== listener);}return this;}
}module.exports = MyEmitter;

核心逻辑:

  • 事件表:用一个对象存储所有事件,key 是事件名,value 是监听器数组。
  • 解绑off 方法通过引用比较来移除监听器。注意,箭头函数和普通函数的引用是不同的,这是常见的坑。
  • 链式调用:所有方法都返回 this,支持 emitter.on('a', fn).on('b', fn2)

避坑提示: 如果在 emit 时,监听器内部又调用了 onoff,会导致数组在遍历过程中被修改。 生产级实现(如 Node.js 的 events 模块)会先复制一份数组再遍历。 这里为了简洁,省略了这一步,但你在实际项目中必须注意。

3. 手写 Router:理解前端路由

Vue Router、React Router,本质都是这个。

// lib/router.js
class MyRouter {constructor() {this.routes = [];this.currentRoute = null;}addRoute(path, handler) {this.routes.push({ path, handler });return this;}navigate(path) {const route = this.routes.find(r => r.path === path);if (route) {this.currentRoute = path;route.handler();} else {console.warn(`Route not found: ${path}`);}}
}module.exports = MyRouter;

进阶技巧: 这个实现只支持精确匹配。 实际项目中,你需要支持参数匹配,如 /user/:id。 可以用正则替换 :([^/]+),然后用 match 方法提取参数。

// 进阶版 navigate
navigate(path) {for (const route of this.routes) {const regex = new RegExp(`^${route.path.replace(/:[^/]+/g, '([^/]+)')}$`);const match = path.match(regex);if (match) {const params = {};const keys = route.path.match(/:([^/]+)/g) || [];keys.forEach((key, i) => {params[key.slice(1)] = match[i + 1];});this.currentRoute = path;route.handler(params);return;}}
}

这段代码,就是所有前端路由框架的核心。 看懂了它,你就理解了 Vue Router 的 params 是怎么来的。

运行与测试

代码写完了,必须跑起来。 不要相信“我觉得能跑”。

test/run.js 内容如下:

const MyPromise = require('../lib/promise');
const MyEmitter = require('../lib/emitter');
const MyRouter = require('../lib/router');// 测试 Promise
console.log('--- Test Promise ---');
const p = new MyPromise((resolve, reject) => {setTimeout(() => resolve('hello'), 100);
});
p.then(res => {console.log('Promise resolved:', res);return new MyPromise(resolve => setTimeout(() => resolve('world'), 100));
}).then(res => {console.log('Chained resolved:', res);
});// 测试 Emitter
console.log('--- Test Emitter ---');
const emitter = new MyEmitter();
const listener = (msg) => console.log('Event:', msg);
emitter.on('test', listener);
emitter.emit('test', 'hello emitter');
emitter.off('test', listener);
emitter.emit('test', 'should not print');// 测试 Router
console.log('--- Test Router ---');
const router = new MyRouter();
router.addRoute('/home', () => console.log('Home page'));
router.addRoute('/user/:id', (params) => console.log('User page, id:', params.id));
router.navigate('/home');
router.navigate('/user/123');

运行方式:

node test/run.js

预期输出:

--- Test Promise ---
Promise resolved: hello
Chained resolved: world
--- Test Emitter ---
Event: hello emitter
--- Test Router ---
Home page
User page, id: 123

如果输出不对,检查三个地方:

  1. Promise 的 then 是否返回了新实例。
  2. Emitter 的 off 是否正确移除了引用。
  3. Router 的正则是否正确匹配了参数。

常见错误:

  • Promiseresolve 后还调用 reject:状态机保护会忽略。
  • Emitter 中监听器是匿名函数:无法被 off 移除,导致内存泄漏。
  • Router 中正则未锚定 ^$:导致 /home 匹配到 /homepage

优化扩展

跑通只是开始。 手写实现的乐趣在于:你能知道哪里可以优化。

1. Promise 的微任务优化 当前用 setTimeout 模拟异步,是宏任务。 真实浏览器中,Promisethen 回调是微任务,优先级高于 setTimeout。 修改方法:

// 替换 setTimeout 为:
Promise.resolve().then(() => {// handleFulfilled 或 handleRejected
});

但注意,这会形成循环依赖。实际项目中,可以用 queueMicrotask(Node.js 11+,现代浏览器支持)。

2. Emitter 的内存泄漏防护 如果监听器持有大量闭包引用,忘记 off 会导致内存泄漏。 可以加一个 maxListeners 限制,超过阈值时警告。

on(event, listener) {if (!this.events[event]) {this.events[event] = [];}if (this.events[event].length >= 10) {console.warn(`Possible memory leak: too many listeners for ${event}`);}this.events[event].push(listener);return this;
}

3. Router 的中间件支持 参考 Express,支持 use 方法。

use(middleware) {this.middlewares = this.middlewares || [];this.middlewares.push(middleware);
}

navigate 中,先执行所有中间件,再执行目标 handler。

4. 单元测试 用 Jest 或 Mocha 写测试。 重点测试边界情况:

  • Promise 链中某一步 reject,后续是否跳过。
  • Emitter 在 emiton 新事件,是否影响当前遍历。
  • Router 参数为空字符串时,正则是否匹配。

5. 打包与发布 用 Rollup 打包成 ES Module 和 CommonJS 两种格式。 加上 TypeScript 类型声明文件 .d.ts。 发布到 npm,供其他项目使用。

小结

又是一年开学季,别只盯着新框架的新特性。 版本升级后 API 全变了,本质是你没理解底层。

手写实现不是怀旧,是修行。

  • 手写 Promise,你懂了异步调度。
  • 手写 Emitter,你懂了事件驱动。
  • 手写 Router,你懂了前端路由。

这三个组件,代码量不到 200 行,但覆盖了前端最核心的机制。 跑通它们,比看十篇框架教程都有用。

Stack Overflow 上有无数关于“为什么我的 Promise 不生效”、“为什么我的事件监听器没触发”的问题。 答案往往不是框架 bug,而是你对底层机制的理解偏差。

现在,打开你的编辑器。 复制上面的代码,跑一遍,改一遍,断点调试一遍。 把每一个 if、每一个 forEach、每一个正则,都搞懂。

你更常用哪种写法?是直接用框架 API,还是喜欢手写底层实现?评论区交流。 说说你在手写过程中踩过的坑,或者你觉得最难理解的部分。 互相交流,比独自摸索快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 19:42:32

加普威th880原理详解 2026最新面试避坑指南

加普威th880原理详解 2026最新面试避坑指南 面试被问原理答不上来,现场直接懵圈?别慌,很多应届生在技术面试中都会遇到这种“卡壳”时刻,尤其是面对像 加普威th880 这类硬件协议或通信机制时,往往只能背概念,讲不清底层逻辑。其实, 2026最新…

作者头像 李华
网站建设 2026/9/21 19:42:15

3步搞定月亮怎么画:从代码到面试的实战项目指南

3步搞定月亮怎么画:从代码到面试的实战项目指南 面试被问原理答不上来,这种尴尬你经历过吗? 别急着慌,很多时候不是没努力,而是缺乏一个能讲透逻辑的实战项目。 今天咱们就用“月亮怎么画”这个看似简单的需求,拆解一个能写进简历的实战项目,让你下次面试能稳稳接住“为什么这么写”的灵魂拷问。…

作者头像 李华
网站建设 2026/9/21 19:42:02

快播伦理电影下载源码解析:3个坑教你搞定后端调试

快播伦理电影下载源码解析:3个坑教你搞定后端调试 复制来的代码跑不通不知道怎么调?别急着删库,90%的问题出在环境依赖和配置上。今天用 快播伦理电影下载 这个典型场景做 源码解析 ,从后端视角拆解下载逻辑,帮你3分钟定位错误根源。 概念速懂:下载流程到底在干嘛…

作者头像 李华
网站建设 2026/9/21 19:41:42

搞懂c4d渲染设置图解原理,这3个坑让你少熬3个通宵

搞懂c4d渲染设置图解原理,这3个坑让你少熬3个通宵 看了一堆教程还是不会写项目?别急,问题不在你笨,在于那些视频只教了“怎么点”,没讲透“为什么”。今天咱们不背参数,直接上 图解原理 ,把C4D渲染设置里最折磨人的三个深坑挖开。…

作者头像 李华
网站建设 2026/9/21 19:41:31

3步搞定最新个税表,手写实现前端计算逻辑避坑指南

3步搞定最新个税表,手写实现前端计算逻辑避坑指南 刚写完几个 CRUD 页面,看着控制台没报错,心里却发虚。很多前端兄弟都卡在“学会语法却不知怎么搭项目”这个死胡同里。你懂了 for 循环,懂 if 判断,但真让你算个工资条,特别是涉及 最新个税表…

作者头像 李华
网站建设 2026/9/21 19:41:28

3个网络ap实战避坑点,源码解析助你面试通关

3个网络ap实战避坑点,源码解析助你面试通关 面试被问网络ap原理答不上来?别慌,这往往是简历上写了“精通Linux网络编程”却卡在基础概念的空洞。很多开发者背熟了API,但一旦面试官追问AP(Access…

作者头像 李华