深入理解 JavaScript Event Loop:从概念到实践的完整探索

前段时间在调试一段异步代码时,我遇到了一个让我困惑的问题:明明看起来逻辑很清晰的代码,输出顺序却和我预期的完全不同。这促使我重新审视 Event Loop 这个概念------它不仅仅是面试时的八股文,更是理解 JavaScript 运行机制的核心钥匙。这篇文章是我的学习总结,希望能和你一起探索从"是什么"到"为什么"再到"怎么用"的完整思考路径。

问题的起源:JavaScript 为什么需要 Event Loop

单线程的设计初衷

JavaScript 诞生于 1995 年,最初是作为浏览器脚本语言来设计的。Brendan Eich 在设计时选择了单线程模型,这个决定看似简单,背后却有深刻的考量。

我的理解是,在浏览器环境中,JavaScript 的主要任务是操作 DOM。如果允许多线程同时操作同一个 DOM 节点,就会出现竞态条件(race condition)------线程 A 正在删除一个节点,线程 B 却在修改它,这会导致难以预测的结果。单线程避免了这个问题,让 DOM 操作变得可预测、可控。

但单线程也带来了一个严峻的问题:如果执行一个耗时操作(比如网络请求、文件读取),整个程序就会被阻塞,用户界面会完全卡死。这显然不可接受。

Event Loop 解决了什么本质问题

Event Loop 的出现,就是为了在单线程的限制下实现"非阻塞 I/O"。它的核心思想是:

  1. 主线程只负责快速执行同步代码
  2. 耗时操作交给浏览器的其他线程处理(如网络线程、定时器线程)
  3. 当耗时操作完成后,通过回调函数通知主线程
  4. Event Loop 不断检查是否有待执行的回调

这是一种协作式多任务 (cooperative multitasking)模型,而不是抢占式多任务(preemptive multitasking)。主线程不会被强制中断,而是在执行完当前任务后,主动去检查是否有新任务。

从 Callback 到 Promise 再到 async/await,JavaScript 异步方案的演进本质上都是在这个模型基础上的语法改进,让异步代码更容易写、更容易读。

核心概念探索

Event Loop 的完整流程

根据我查阅的资料,一个完整的 Event Loop 循环包含以下几个关键部分:

  1. 调用栈(Call Stack) :存放正在执行的函数
  2. 任务队列(Task Queue / Macrotask Queue) :存放宏任务
  3. 微任务队列(Microtask Queue) :存放微任务
  4. Web APIs:浏览器提供的异步能力(setTimeout、fetch 等)

一个标准的 Event Loop 循环是这样的:

graph TB A[开始执行脚本] --> B[执行同步代码] B --> C{调用栈是否为空?} C -->|否| B C -->|是| D[执行所有微任务] D --> E{微任务队列是否为空?} E -->|否| D E -->|是| F[浏览器渲染 UI 可选] F --> G[从宏任务队列取一个任务] G --> H{是否有宏任务?} H -->|是| B H -->|否| I[等待新任务] I --> G

这个流程可以简化为一句话:执行一个宏任务 → 清空所有微任务 → (可能的 UI 渲染)→ 执行下一个宏任务

宏任务(Macrotask)与微任务(Microtask)

在 JavaScript 中,异步任务被分为两类:

宏任务包含:

  • setTimeout / setInterval
  • setImmediate(Node.js 环境)
  • I/O 操作
  • UI 渲染(浏览器环境)
  • requestAnimationFrame(虽然它比较特殊,不完全算宏任务)

微任务包含:

  • Promise.then / catch / finally
  • async/await(本质是 Promise)
  • MutationObserver
  • queueMicrotask
  • process.nextTick(Node.js 环境,优先级最高)
javascript 复制代码
// 环境:浏览器 / Node.js 18+
// 场景:基础的宏任务与微任务执行顺序

console.log('1'); // 同步代码

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

console.log('4'); // 同步代码

// 输出顺序:1 → 4 → 3 → 2

为什么是这个顺序?

  1. 首先执行所有同步代码:输出 14
  2. 同步代码执行完毕,调用栈清空
  3. 清空微任务队列:执行 Promise.then,输出 3
  4. 从宏任务队列取出 setTimeout 的回调,输出 2

执行优先级的设计思想

为什么微任务要优先于宏任务执行?我的理解是,这样设计有几个好处:

1. 保证状态的一致性

微任务通常用于处理"紧急"的后续操作。比如 Promise.then 是对 Promise 状态变化的响应,应该尽快执行,而不是等到下一个宏任务周期。

2. 提升响应速度

对于用户交互相关的逻辑,如果放在微任务中,能更快地响应用户操作。

3. 批量处理的机会

在一个宏任务执行完后,可以通过微任务批量处理相关的状态更新,然后统一触发 UI 渲染,而不是每次状态变化都渲染一次。

但这也带来了风险:如果微任务队列一直不为空(比如微任务中又添加微任务),会导致宏任务饥饿,甚至阻塞 UI 渲染。

从原理到实践:代码执行顺序分析

基础场景:Promise + setTimeout

让我们从一个经典的面试题开始:

javascript 复制代码
// 环境:浏览器 / Node.js 18+
// 场景:混合异步代码执行顺序

console.log('start');

setTimeout(() => {
  console.log('timeout1');
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

setTimeout(() => {
  console.log('timeout2');
}, 0);

console.log('end');

// 输出:start → end → promise1 → promise2 → timeout1 → timeout2

执行过程分析:

步骤 动作 调用栈 微任务队列 宏任务队列 输出
1 执行同步代码 console.log('start') [] [] start
2 注册定时器 - [] [timeout1] -
3 Promise.resolve() - [promise1] [timeout1] -
4 注册定时器 - [promise1] [timeout1, timeout2] -
5 执行同步代码 console.log('end') [promise1] [timeout1, timeout2] end
6 清空微任务 promise1 [promise2] [timeout1, timeout2] promise1
7 继续清空微任务 promise2 [] [timeout1, timeout2] promise2
8 执行宏任务 timeout1 [] [timeout2] timeout1
9 执行宏任务 timeout2 [] [] timeout2

这个表格清晰地展示了 Event Loop 的执行过程。

进阶场景:async/await 的本质

很多人认为 async/await 是全新的异步机制,但实际上它只是 Promise 的语法糖。

javascript 复制代码
// 环境:浏览器 / Node.js 18+
// 场景:async/await 执行顺序

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end'); // 这行代码等价于 Promise.then
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

关键理解:await 后面的代码会被包装成微任务

上面的 async1 函数可以等价转换为:

javascript 复制代码
// 等价转换:展示 async/await 的本质
function async1() {
  console.log('async1 start');
  return async2().then(() => {
    console.log('async1 end');
  });
}

这就解释了为什么 async1 end 会在 script end 之后、promise2 之前输出。

复杂场景:多种异步混合

现在来看一个更复杂的例子:

javascript 复制代码
// 环境:浏览器
// 场景:Promise、setTimeout、requestAnimationFrame 混合

console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);

new Promise((resolve) => {
  console.log('4');
  resolve();
}).then(() => {
  console.log('5');
}).then(() => {
  console.log('6');
});

requestAnimationFrame(() => {
  console.log('7');
});

console.log('8');

// 输出:1 → 4 → 8 → 5 → 6 → 7 → 2 → 3

这里有个有趣的点:requestAnimationFrame 的执行时机。它不是宏任务,也不是微任务,而是在浏览器重绘之前执行。大致的执行顺序是:

同步代码 → 微任务 → rAF 回调 → 浏览器渲染 → 宏任务

危险场景:微任务无限循环

前面提到微任务会"插队"执行,如果不小心,可能会造成主线程阻塞:

javascript 复制代码
// 环境:浏览器 / Node.js
// 场景:危险!微任务无限循环
// 警告:这段代码会阻塞主线程,不要在生产环境运行

function blockingMicrotask() {
  Promise.resolve().then(() => {
    console.log('microtask');
    blockingMicrotask(); // 递归添加微任务
  });
}

setTimeout(() => {
  console.log('This will never execute');
}, 0);

blockingMicrotask();

// 结果:不断输出 microtask,setTimeout 永远不会执行

这是因为微任务队列永远不会清空,Event Loop 永远到不了"执行宏任务"这一步。

相比之下,宏任务的无限循环不会阻塞其他宏任务:

javascript 复制代码
// 环境:浏览器 / Node.js
// 场景:宏任务递归不会阻塞其他宏任务

function recursiveTimeout() {
  console.log('timeout');
  setTimeout(recursiveTimeout, 0);
}

setTimeout(() => {
  console.log('I can still execute');
}, 0);

recursiveTimeout();

// 结果:两个定时器会交替执行

实际应用场景思考

场景 1:利用微任务优化性能------批量更新

在实际开发中,一个常见的优化场景是:避免多次触发 DOM 更新。

比如你有一个状态管理系统,多个组件同时更新状态,如果每次状态变化都立即触发重新渲染,性能会很差。一个常见的优化思路是在微任务中批量更新

这正是 Vue 的 nextTick 和 React 18 之前的批量更新机制的核心思想。

javascript 复制代码
// 环境:浏览器
// 场景:手写一个简易的批量更新调度器

class Scheduler {
  constructor() {
    this.pending = false;
    this.updates = [];
  }

  scheduleUpdate(fn) {
    this.updates.push(fn);
    
    if (!this.pending) {
      this.pending = true;
      // 使用微任务批量执行所有更新
      queueMicrotask(() => {
        this.flushUpdates();
      });
    }
  }

  flushUpdates() {
    const currentUpdates = this.updates.slice();
    this.updates = [];
    this.pending = false;
    
    currentUpdates.forEach(fn => fn());
  }
}

// 使用示例
const scheduler = new Scheduler();

console.log('start');

scheduler.scheduleUpdate(() => console.log('update 1'));
scheduler.scheduleUpdate(() => console.log('update 2'));
scheduler.scheduleUpdate(() => console.log('update 3'));

console.log('end');

// 输出:start → end → update 1 → update 2 → update 3
// 三个更新被批量处理,只触发一次微任务

这个例子展示了如何利用微任务的特性:在当前同步代码执行完后、UI 渲染前,批量处理所有状态更新。

场景 2:requestAnimationFrame 与 requestIdleCallback

在做动画或者性能优化时,requestAnimationFramerequestIdleCallback 是两个非常有用的 API。

requestAnimationFrame (rAF):在浏览器重绘之前执行

适用场景:

  • 动画
  • 需要与视觉更新同步的操作
javascript 复制代码
// 环境:浏览器
// 场景:使用 rAF 实现流畅动画

let start = null;
const element = document.getElementById('box');

function animate(timestamp) {
  if (!start) start = timestamp;
  const progress = timestamp - start;
  
  element.style.transform = `translateX(${Math.min(progress / 10, 200)}px)`;
  
  if (progress < 2000) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

requestIdleCallback (rIC):在浏览器空闲时执行

适用场景:

  • 非紧急的后台任务
  • 数据预加载
  • 分析统计
javascript 复制代码
// 环境:浏览器(注意:Safari 不支持 rIC)
// 场景:在浏览器空闲时处理非紧急任务

function processLargeDataWhenIdle(data) {
  function processChunk(deadline) {
    while (deadline.timeRemaining() > 0 && data.length > 0) {
      const item = data.shift();
      // 处理数据项
      console.log('Processing:', item);
    }
    
    if (data.length > 0) {
      requestIdleCallback(processChunk);
    }
  }
  
  requestIdleCallback(processChunk);
}

setTimeout vs rAF 的对比:

javascript 复制代码
// 环境:浏览器
// 场景:对比 setTimeout 和 rAF 的执行时机

let count = 0;

// setTimeout:不与渲染同步,可能跳帧
function animateWithTimeout() {
  count++;
  element.textContent = count;
  
  if (count < 60) {
    setTimeout(animateWithTimeout, 16); // 约 60fps
  }
}

// rAF:与浏览器渲染同步,更流畅
function animateWithRAF() {
  count++;
  element.textContent = count;
  
  if (count < 60) {
    requestAnimationFrame(animateWithRAF);
  }
}

场景 3:任务优先级调度

如果让你设计一个任务调度器,需要支持不同优先级的任务,你会如何利用 Event Loop?

这其实是 React Scheduler 的核心思想。一个简化的实现思路:

javascript 复制代码
// 环境:浏览器
// 场景:基于宏任务和微任务实现简易任务调度器

class TaskScheduler {
  constructor() {
    this.highPriorityQueue = [];    // 微任务
    this.normalPriorityQueue = [];  // 宏任务(MessageChannel)
    this.lowPriorityQueue = [];     // requestIdleCallback
  }

  scheduleTask(task, priority = 'normal') {
    switch (priority) {
      case 'high':
        // 高优先级:使用微任务,会在当前宏任务结束后立即执行
        this.highPriorityQueue.push(task);
        queueMicrotask(() => this.flushHighPriority());
        break;
        
      case 'normal':
        // 普通优先级:使用宏任务
        this.normalPriorityQueue.push(task);
        this.scheduleNormalTask();
        break;
        
      case 'low':
        // 低优先级:使用 requestIdleCallback
        this.lowPriorityQueue.push(task);
        this.scheduleLowPriorityTask();
        break;
    }
  }

  flushHighPriority() {
    while (this.highPriorityQueue.length > 0) {
      const task = this.highPriorityQueue.shift();
      task();
    }
  }

  scheduleNormalTask() {
    setTimeout(() => {
      if (this.normalPriorityQueue.length > 0) {
        const task = this.normalPriorityQueue.shift();
        task();
      }
    }, 0);
  }

  scheduleLowPriorityTask() {
    if (typeof requestIdleCallback !== 'undefined') {
      requestIdleCallback((deadline) => {
        while (deadline.timeRemaining() > 0 && this.lowPriorityQueue.length > 0) {
          const task = this.lowPriorityQueue.shift();
          task();
        }
      });
    } else {
      // Fallback for Safari
      setTimeout(() => {
        if (this.lowPriorityQueue.length > 0) {
          const task = this.lowPriorityQueue.shift();
          task();
        }
      }, 100);
    }
  }
}

// 使用示例
const scheduler = new TaskScheduler();

console.log('Start');

scheduler.scheduleTask(() => console.log('Low priority'), 'low');
scheduler.scheduleTask(() => console.log('Normal priority'), 'normal');
scheduler.scheduleTask(() => console.log('High priority'), 'high');

console.log('End');

// 输出:Start → End → High priority → Normal priority → Low priority

这个调度器展示了如何利用不同的异步机制实现任务优先级。当然,真实的 React Scheduler 要复杂得多,它还需要处理任务中断、时间切片等问题。

跨端差异:浏览器 vs Node.js

Node.js Event Loop 的阶段模型

Node.js 的 Event Loop 和浏览器有明显的区别。它采用了 libuv 库,将 Event Loop 分为多个阶段:

graph TB A[timers] --> B[pending callbacks] B --> C[idle, prepare] C --> D[poll] D --> E[check] E --> F[close callbacks] F --> A style D fill:#f9f,stroke:#333,stroke-width:2px

各阶段说明:

  1. timers :执行 setTimeoutsetInterval 的回调
  2. pending callbacks:执行延迟到下一个循环迭代的 I/O 回调
  3. idle, prepare:仅内部使用
  4. poll:检索新的 I/O 事件,执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它由 timers 和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞
  5. check :执行 setImmediate() 回调
  6. close callbacks :执行关闭的回调函数,如 socket.on('close', ...)

微任务的特殊性: 在每个阶段之间,都会清空微任务队列。而 process.nextTick 比其他微任务优先级更高。

关键差异点

1. setImmediate vs setTimeout(fn, 0)

在浏览器中,只有 setTimeout。在 Node.js 中,setImmediatesetTimeout(fn, 0) 的执行顺序取决于调用它们的上下文:

javascript 复制代码
// 环境:Node.js
// 场景:setImmediate vs setTimeout 的执行顺序

// 在主模块中,顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

// 可能输出:timeout → immediate
// 也可能输出:immediate → timeout

// 但在 I/O 回调中,setImmediate 总是先执行
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});

// 一定输出:immediate → timeout

为什么会这样?

在 I/O 回调中,当前处于 poll 阶段,下一个阶段是 check(执行 setImmediate),然后才是 timers(执行 setTimeout)。

2. process.nextTick 的特殊性

process.nextTick 不属于 Event Loop 的任何阶段,它的优先级最高:

javascript 复制代码
// 环境:Node.js 18+
// 场景:process.nextTick 的优先级

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// 输出:nextTick → promise

process.nextTick 会在当前操作完成后立即执行,甚至在微任务队列之前。

为什么会有这些差异?

浏览器和 Node.js 的设计目标不同:

浏览器:

  • 需要频繁的 UI 渲染
  • 用户交互响应优先级高
  • 更关注视觉流畅性

Node.js:

  • I/O 密集型场景
  • 没有 UI 渲染需求
  • 更关注吞吐量和并发处理能力

这导致它们在 Event Loop 的实现上有不同的侧重点。

延伸与发散

异步方案的演进脉络

从 Callback 到 Promise 再到 async/await,JavaScript 的异步方案经历了什么样的演进?

Callback 时代:

  • 问题:回调地狱(Callback Hell)
  • 难以处理错误
  • 代码可读性差

Promise 时代:

  • 解决了回调地狱
  • 统一的错误处理(.catch)
  • 支持链式调用

async/await 时代:

  • 以同步的方式写异步代码
  • 更直观的控制流
  • 更好的错误处理(try/catch)

这种演进体现了一个思想:让异步代码更像同步代码,降低心智负担

未来可能的方向?我看到了一些有趣的提案:

  • React 的 Concurrent Mode:时间切片、可中断渲染
  • Scheduler API:标准化的任务调度接口
  • Web Workers 的进一步普及

在 AI 辅助编程时代,理解 Event Loop 还有意义吗?

这是我最近一直在思考的问题。既然 AI 可以帮我写异步代码,甚至帮我 debug,那我还需要深入理解 Event Loop 吗?

经过一段时间的思考和实践,我有一些不成熟的想法:

思考 1:理解原理 = 更好地驾驭 AI

AI 可以写代码,但你需要判断代码是否正确。如果不理解 Event Loop,你如何 review AI 生成的异步逻辑?

比如,AI 给你生成了这样的代码:

javascript 复制代码
// AI 生成的代码
async function fetchData() {
  const data = await fetch('/api/data');
  setTimeout(() => {
    processData(data);
  }, 0);
}

如果你理解 Event Loop,就会意识到这里用 setTimeout 可能不是最优解。为什么要把 processData 放到宏任务?直接在 await 后面执行不是更好吗?

javascript 复制代码
// 优化后的代码
async function fetchData() {
  const data = await fetch('/api/data');
  processData(data); // 直接执行,不需要 setTimeout
}

思考 2:底层理解帮助你提出更好的问题

向 AI 提问时,如果你能准确描述问题,AI 的回答也会更精准。

含糊的提问:

"为什么我的 Promise 没有执行?"

精准的提问:

"我有一个 Promise.then 回调,但在 setTimeout 的回调里注册的,为什么它会在下一个宏任务才执行,而不是当前宏任务结束后立即执行?"

理解原理让你的提问更专业,AI 的回答也更有针对性。

思考 3:复杂场景下的判断力

AI 生成的代码在简单场景下可能没问题,但在复杂的异步编排、性能优化场景下呢?

比如,AI 可能给你提供三种不同的异步方案:

javascript 复制代码
// 方案 1:全部用 Promise.all
async function loadData() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  render(users, posts, comments);
}

// 方案 2:分步加载
async function loadData() {
  const users = await fetchUsers();
  render({ users });
  
  const posts = await fetchPosts();
  render({ users, posts });
  
  const comments = await fetchComments();
  render({ users, posts, comments });
}

// 方案 3:优先级加载
async function loadData() {
  const usersPromise = fetchUsers();
  const postsPromise = fetchPosts();
  
  const users = await usersPromise;
  render({ users });
  
  const posts = await postsPromise;
  render({ users, posts });
  
  const comments = await fetchComments();
  render({ users, posts, comments });
}

哪个方案更好?这取决于你的具体需求:

  • 方案 1:最快,但需要等所有数据都加载完才渲染
  • 方案 2:渐进式渲染,但串行加载速度慢
  • 方案 3:兼顾并行加载和渐进式渲染

如果你理解 Event Loop 和异步编排的原理,就能快速判断哪个方案更适合你的场景。

思考 4:知识体系 vs 代码片段

AI 给你的是解决方案,不是知识体系。理解 Event Loop 是构建你自己的前端知识网络的一部分。

这个网络帮助你:

  • 触类旁通:理解 React 的并发模式、Vue 的响应式原理
  • 快速定位问题:线上 bug 的 root cause 分析
  • 技术决策:选型时的判断依据

我的阶段性结论:

AI 是工具,不是替代。理解原理让你从"使用工具"变成"驾驭工具"。

在 AI 时代,底层知识可能不是用来"手写代码",而是用来"判断、决策、优化"。就像你不需要会造车,但作为司机,你需要理解油门、刹车、方向盘的原理,才能把车开好。

当然,这个结论可能也会随着技术的发展而改变。保持开放的心态,持续学习,或许是应对变化的唯一不变法则。

待探索的问题

在研究 Event Loop 的过程中,我产生了一些新的疑问:

  1. Web Workers 与主线程的 Event Loop 如何协作?

    • Worker 有自己独立的 Event Loop 吗?
    • postMessage 的消息是宏任务还是微任务?
  2. Service Worker 的事件循环有什么特殊之处?

    • 它如何拦截网络请求?
    • 如何与页面的 Event Loop 交互?
  3. 在微前端场景下,多个应用的 Event Loop 如何互不干扰?

    • 沙箱机制如何隔离异步任务?
    • 多个应用如何共享浏览器的事件循环?
  4. 浏览器的渲染时机与 Event Loop 的关系?

    • 16.6ms 的渲染周期是如何与 Event Loop 协调的?
    • requestAnimationFrame 为什么能保证在渲染前执行?

这些问题我还没有完全想清楚,如果你有见解,欢迎交流。

小结

Event Loop 是理解 JavaScript 异步编程的核心。从"为什么需要"到"是什么"再到"怎么用",这个探索过程让我对 JavaScript 的运行机制有了更深的理解。

但我也意识到,理解原理不是目的,而是手段。真正的目的是:

  • 写出更高效、更可维护的代码
  • 更快地定位和解决问题
  • 在技术选型时做出明智的决策
  • 在 AI 时代,成为驾驭工具的人,而不是被工具驾驭

这篇文章更多是我的学习笔记和思考过程,而非标准答案。技术在不断演进,我的理解也可能有偏差。如果你有不同的看法或补充,欢迎交流讨论。

最后留一个开放性问题:你在实际开发中遇到过哪些与 Event Loop 相关的有趣问题或坑?你是如何解决的?

参考资料

相关推荐
程序员阿峰2 小时前
WebSocket 原理解析
前端
Lee川2 小时前
JavaScript 继承进化史:从原型链的迷雾到完美的寄生组合
前端·javascript·面试
米饭同学i2 小时前
微信小程序实现故事线指引动画效果
前端
阿懂在掘金2 小时前
为什么写 Vue 强烈建议用 Setup?除了复用,更是代码组织
前端·vue.js
sorryhc2 小时前
我让 AI 帮我写了一个 Code Agent!
前端·openai·ai编程
工边页字2 小时前
面试官:请详细介绍下AI中的token,越详细越好!
前端·人工智能·后端
anyup2 小时前
月销 8000+,uView Pro 让 uni-app 跨端开发提速 10 倍
前端·uni-app·开源
码路飞2 小时前
热榜全是 OpenClaw,但我用 50 行 Python 就造了个桌面 AI Agent 🤖
java·javascript
前端Hardy3 小时前
别再忽略 Promise 拒绝了!你的 Node.js 服务正在“静默自杀”
前端·javascript·面试