阅读视图

发现新文章,点击刷新页面。

Vol1.人才留言板|第一期:这20家AI明星公司正在等你

图源:36kr

每周精选30+优质岗位,覆盖AI、硬科技等领域,帮你找到下一个职业爆发点。

本期合作企业:红杉中国、高瓴、鼎晖、经纬被投企业|B轮及以上占比65%。

卷首语

卷首语我们见过太多这样的场景:一家硬科技公司融完B轮,创始人却在朋友圈悄悄挂出招聘启事;一个专注大模型的算法专家,翻遍猎头推荐却找不到真正懂技术的团队。

这不是供需错配,而是信息失真。

过去十年,36氪记录了数千家公司的成长轨迹。我们看过他们最早的BP,见证他们一轮轮融资,知道哪家公司的技术栈真正扎实,哪个创始团队对人才有足够耐心。这些认知,不该只留在报道里。

所以有了“人才留言板”。

我们想做一个更笨拙也更真诚的连接:一边是经过多轮融资、资金充沛、真正尊重技术的公司——尤其是AI、机器人领域的佼佼者,他们手握充足弹药,唯独渴望能一起冲锋陷阵的顶尖人才;一边是愿意用硬实力换取长期回报的技术高手。36氪不做简单的简历搬运,而是用我们对行业的理解,帮双方建立一条更短、更准的通道。

这里没有花哨的职位包装,只有我们了解并愿意背书的团队。

同时,我们也欢迎人才主动“留言”——无论你是名校出身的算法大神、深耕硬件的架构师,还是操盘过亿级业务、身经百战的职场高管,只要你想找到真正看懂你价值的公司和团队,欢迎联系我们。

—— 36氪“人才留言板”

图源:36kr

本期亮点速览

图源:36kr

图源:36kr

精选岗位

| 金山办公 · 已上市 · AI应用

一句话卖点:

做难而正确的事,向上生长

地点:北京

招聘职位:

AI应用研发工程师(数据结构|智能交互系统|NLP|LLM|25-40K)

职位介绍:

技术立业的民族情怀、极具竞争力的薪酬体系

清晰的职业发展路径与开放包容的组织文化

丨中控技术 · 已上市 · 工业智控

一句话卖点:

工业自动化领军企业,全栈技术实战平台,多元成长通道

地点:杭州

招聘职位:

高级软件测试工程师(锂电池原理|本科及以上|15-17K)

职位介绍:

工业自动化领军企业、“高壁垒技术平台”与“人性化成长生态”的完美结合

前沿技术落地,解决石化、化工等复杂工业场景的“真问题”

丨宇树科技 · IPO · 具身智能

一句话卖点:

全球最优秀的机器人团队,等待你的加入,让我们一起变革世界!

地点:杭州

招聘职位:

深度强化学习算法工程师(C++/Python|机器人仿真|本科及以上|25-50K)

机器人运动控制算法工程师(机器人控制/自动控制/机械电子|硕士及以上|30-60K)

职位介绍:

全球人形机器人领跑者、打破年龄限制

打破常规的职业机会、极具竞争力的薪酬回报以及产教融合的成长生态

丨声网agora · 已上市 · 人工智能

一句话卖点:

加入我们,让实时互动像空气和水一样无处不在

地点:上海

招聘职位:

实时AI应用开发工程师(计算机软件|Golang/Python|本科及以上|薪资面议)

音频算法工程师(声纹/3A/ASR/TTS|Python/C/C++|硕士及以上|薪资面议)

职位介绍:

全球领先的实时互动技术壁垒、顶尖的精英团队

清晰的全球化职业发展通道

丨云深处科技 · Pre-IPO · 具身智能

一句话卖点:

杭州“六小龙”之一,全球具身智能行业应用引领者,多项性能全球领先

地点:杭州

招聘职位:

算法工程师(感控融合/具身导航/SLAM|VLM/Diffusion优先|硕士及以上|30-50K·14薪)

电机驱动工程师(无刷电机FOC控制|嵌入式开发|本科及以上|20-40K·14薪)

机器人产品经理(实施工具链|现场效率提升|本科及以上|20-40K·14薪)

职位介绍:

处于上市前窗口期,期权价值可期

核心产业中推动机器人技术规模化落地

承担国家级项目,参与制定行业标准,收获技术深度与行业影响力的双重成长

丨追觅集团 · D轮 · 智能硬件

一句话卖点:

敢于追梦 敢于行动

地点:苏州

招聘职位:

电池研发岗(锂电池原理|本科及以上|薪酬面议)

职位介绍:

新人直接参与核心项目,而非从基础工作做起、独特培养机制

高难度项目与内部竞合,为新人提供顶级的成长快车道

丨酷哇机器人 · D轮 · 通用 AI 机器人

一句话卖点:

让每一颗种子都发芽

地点:芜湖

招聘职位:

机械工程师(机械/车辆工程|运动机构设计|本科及以上|薪资面议)

规划算法工程师(计算机/数学/控制/车辆工程|轨迹优化方向|硕士及以上|薪资面议)

职位介绍:

已跑通的商业闭环带来职业确定性、Physical AI 前沿技术落地带来成长上限

接触行业最顶尖的通用大模型技术、硬科技领域实现“自我造血”的样本

丨松延动力 · B轮 · 具身智能

一句话卖点:

专注双足机器人强化学习运动控制,深耕算法极限性能探索

地点:北京

招聘职位:

强化学习运控工程师(双足运动控制|RL/IL方向|硕士及以上|30-60K)

职位介绍:

2026马年春晚“小布米”、专属量产基地、全产业链整合优势

团队以95后为主、“技术+产能+产业资源”三重优势、“双足+仿生”双线驱动

丨阶跃星辰 · B轮 · AI大模型

一句话卖点:

聚焦多模态通用AI,凭高效模型、深度终端绑定与多元产业落地获资本青睐

地点:北京/上海

招聘职位:

GUI Agent算法研究员(大模型应用|分布式训练|博士优先|薪资面议)

AI Agent系统工程师(AI应用工程|多框架经验|本科及以上|薪资面议)

职位介绍:

投身通用人工智能(AGI)的终极探索,参与构建行业领先的大模型矩阵

与领域内顶尖人才共事,在技术驱动的环境中获得前沿的全栈研发视野和学术影响力

丨银河通用机器人 · B轮 · 具身智能

一句话卖点:

市场领先的具身多模态大模型机器人,2026年央视春晚指定合作伙伴

地点:北京

招聘职位:

机器人遥操算法专家(主从/VR/动捕|全身控制|硕士优先|薪资面议)

机器人应用开发工程师(具身智能&情感交互|多模态行为设计|薪资面议)

职位介绍:

参与国家级标杆项目,央视春晚指定合作机器人

基于顶尖硬件与自研大模型,攻克核心难题,实现从算法到产品的完整闭环

丨小雨智造 · B轮 · 具身智能

一句话卖点:

工业级具身智能,已在真实产线规模化验证与世界500强深度合作。

地点:北京

招聘职位:

应用软件工程类(35k-55k)

机器人本体类(35k-55k)

机器人感知硬件类现场解决方案类(35k-55k)

职位介绍:

聚焦“工业非标场景”的落地策略,“一脑多形”技术架构的提出

清晰的技术愿景与工程闭环能力,技术成果在真实产线(如建筑钢结构、航空航天、船舶制造)中规模化应用的可能性

丨魔法原子 · B轮 · 具身智能

一句话卖点:

为梦想全力以赴

地点:无锡

招聘职位:

战略BP(咨询/科技企业战略/商业分析|本科及以上|薪酬面议)

运动控制算法专家(自动化/机械工程|机器人控制|硕士及以上|薪酬面议)

电控硬件工程师(电子/电气|硬件电路设计|本科及以上|薪酬面议)

职位介绍:

全栈自研能力、高爆发技术落地、开放生态体系与稀缺行业地位

顶尖薪酬与激励机制、全球顶尖团队构成

丨智元机器人 · A轮 · 具身智能

一句话卖点:

AI+机器人融合创新,业内唯一实现全产品系列、全场景布局

地点:北京/上海/深圳

招聘职位:

多模态交互大模型专家(机器人交互|VLM/VLA方向|本科及以上|薪资面议)

世界模型算法专家(具身智能/视频生成|Transformer/Diffusion|硕士及以上|薪资面议)

AI编译器工程师(自研编译器|TVM/TensorRT经验|薪资面议)

职位介绍:

“全栈自研+生态构建”的路径、“小脑+大脑”的完整解决方案、覆盖从工业到消费端

与“华为系”高管、顶尖科学家、产业精英团队共事

丨赛博格机器人 · A轮 · 具身智能

一句话卖点:

我们基于“科技服务人类,解决高危工种”为初心,用技术赋能百业,为中国新质生产力,中国高科技的崛起,贡献力量。

地点:深圳

招聘职位:

机器人测试工程师(计算机/软件工程|本科及以上|薪资面议)

职位介绍:

顶尖科学家领衔的“硬核”技术高地、聚焦工业刚需的实战场景

极高密度的智力集群、极具竞争力的薪酬回报

丨诺瓦聚变 · 天使+ 轮 · 核聚变

一句话卖点:

站在能源革命的历史拐点,我们提供的不只是一个职位,而是让你在创造人类未来百年基业的同时,成为这一万亿美金级市场的定义者和早期开拓者。

地点:上海

招聘职位:

诊断(高级)工程师(等离子体物理|核聚变|硕士及以上|薪资面议)

高压脉冲电源(高级)工程师(高电压|电气工程|高压脉冲电源|硕士优先|薪资面议)

等离子体实验运行(高级)工程师(等离子体|核科学|硕士及以上|薪资面议)

电子学工程师(电子信息工程|信息工程|硕士及以上|薪资面议)

职位介绍:

推动“人造太阳”从科研走向产业落地、“Fusion for AI”使命

团队汇聚国际顶尖科学家与工程师,融合核聚变与人工智能技术,推动“AI+聚变”交叉创新

丨乐享科技 · 天使+轮 · 具身智能

一句话卖点:

消费级具身智能产品开创者,两款原型即将面市

地点:上海/苏州/深圳/北京

招聘职位:

具身智能全栈工程师(端侧性能|仿真闭环与自动化|20-50K)

机械臂运控算法工程师(运动控制算法|本科及以上|20-40K)

职位介绍:

定义消费级机器人新赛道,全程参与从产品原型到规模量产的完整征程

与清北、中科院、CMU等顶尖背景团队并肩

丨维他动力 · 天使轮 · 具身智能

一句话卖点:

国内首家消费级具身智能公司,成立一年完成3亿融资,百人团队快速扩张

地点:北京/上海

招聘职位:

机器人端到端算法工程师(VLA/导航|多模态大模型|硕士优先|薪资面议)

机器人运控算法工程师(强化学习|足式机器人|Isaac Gym经验|硕士优先|薪资面议)

职位介绍:

从0到1打造产品、实现技术到产品的直接转化

百人明星团队,充足的资金支持

丨Xhorse · 桌面CNC

一句话卖点:

技术全栈能力,产品矩阵完整,长期稳定的发展空间和赛道优势

地点:深圳

招聘职位:

Agent工程师(人工智能|VLM/VLA方向|本科及以上|年薪本科15-20万,硕士20-45万)

数字IC设计工程师(固体电子学|集成电路工程|硕士及以上|年薪硕士20-45万,博士45万以上)

FPGA工程师(FPGA开发流程|Verilog语言|硕士及以上|年薪硕士20-45万,博士45万以上)

职位介绍:

深圳老牌机床企业、连续多年研发投入增幅保持在50%以上、软硬件全线自研

深耕智能制造核心领域、参与从底层算法到硬件设计的全链条研发

丨璇玑动力 · 具身智能

一句话卖点:

璇玑动力在机器人核心零部件、运动控制算法、感知系统等领域技术领先,团队源自全球顶尖硬科技企业,具备机器人从设计、研发到量产落地的全链路实战能力。

地点:深圳

招聘职位:

机器人运动控制算法工程师(机器人控制|自动控制|本科及以上|50-80K)

SLAM算法工程师(C/C++|OpenCV/Eigen/GTSAM/Ceres|本科及以上|40-60K)

职位介绍:

完成近亿元天使轮融资,资金用于行业级机器人量产与消费级新品研发

聚焦智能机器人“全栈自研”,构建从电机、驱动器到运动控制算法的完整技术闭环

图源:36kr

投递或留言规则

岗位投递

邮件标题:岗位投递+公司/姓名+职位(例如:36氪推荐+深光科技+光学工程师),发送至:rclyb@36kr.com

投递须知

所有简历由36氪统一接收,筛选后直达企业HR

每人最多可投递3个岗位,请在邮件正文注明优先顺序

投递后7个工作日内未回复可视为未通过筛选,不再另行通知

公司投稿

投递内容:公司名称、公司简介、一句话卖点、轮次、招聘职位、职位介绍、地点、联系人及联系方式(以上信息可选公开)

发送至:rclyb@36kr.com,邮件标题注明“公司投稿”

人才自荐

适用人群:希望被好公司发现的顶尖技术人才、资深业务高管

投递内容:个人背景(教育/工作经历/核心业绩)、专业方向、意向公司类型/地点、联系方式(可选公开或私密)

发送至:rclyb@36kr.com,邮件标题注明“人才自荐”。我们将精选优质人才,在征得同意后通过栏目“广而告之”

所有投递默认接受36氪筛选与编辑,如需匿名请备注

图源:36kr

中上协:2月全市场总市值再创历史新高,突破116.8万亿

36氪获悉,中上协发布2月统计月报。以2月末收盘价计算,全市场总市值再创历史新高,突破116.8万亿,同比增长34%。其中,制造业、科学研究和技术服务业总市值同比增幅均超40%。2月,超千亿市值公司新增12家,超百亿市值公司新增67家,20亿以下市值公司数量进一步减少。2月末,上市公司市值中位数72.53亿元,较年初提升11%。

南宁轨道未来交通建设公司注册资本增至约29.5亿元

36氪获悉,爱企查App显示,近日,南宁轨道未来交通建设有限公司发生工商变更,注册资本由约21.6亿元人民币增至约29.5亿元人民币,增幅约36%,同时新增南宁市人民政府国有资产监督管理委员会为股东。

你删过 lock 文件吗?聊聊包管理器迁移中 90% 的人会踩的坑

"删掉 node_modulespackage-lock.json,重新 npm install 一下。"

这句话你一定听过,甚至自己也说过。遇到依赖安装报错,删 lock 重装是最常见的"万能解法"。大部分时候确实管用——但它管用的原因和你想的不一样,而且在某些场景下,这个操作的代价比你预期的要大得多。

最近越来越多的项目开始从 npm 迁移到 pnpm。迁移本身不复杂,但很多人的做法是直接删掉 package-lock.json,然后 pnpm install。对于小项目,这通常没问题。但如果你的项目有几百个依赖、跑在生产环境、团队多人协作——这样做可能会引入一些很难排查的问题。

这篇文章聊的就是这个:lock 文件到底在锁什么,删掉它意味着什么,以及迁移包管理器时怎么做才是安全的。

lock 文件在锁什么

package.json 里的版本号不是精确版本,而是一个范围:

{
  "dependencies": {
    "react": "^18.3.1",
    "axios": "~1.7.0"
  }
}

^18.3.1 允许安装 18.3.118.x.x 之间的任何版本,~1.7.0 允许 1.7.01.7.x。也就是说,同一份 package.json,今天装和三个月后装,拿到的依赖版本可能完全不同。

而 lock 文件记录的是某一次 install 之后所有依赖的精确版本——不光是你在 package.json 里写的那几个,还包括它们背后的几十上百个传递依赖。

一句话总结:package.json 描述意图,lock 文件记录事实。

有了 lock 文件,团队成员用 npm ci(或 pnpm install --frozen-lockfile)安装时,拿到的依赖版本和你本地测试通过的完全一致。CI 构建、生产部署,都是同一份版本快照。

semver 是个"君子协议"——很多包不遵守

你可能会想:用 ^ 锁定大版本,minor 和 patch 升级不是应该向下兼容吗?

理论上是。但现实中,不少知名包在 patch 或 minor 版本里引入过 breaking change:

  • TypeScript 明确声明不遵守 semver。它的 minor 版本(比如 5.35.4)经常改变类型推断行为,一次升级可能导致几十个编译错误。
  • esbuild 长期处于 0.x 阶段,按 semver 规范 0.x 的任何变更都可能是 breaking,但很多打包工具用 ^0.21.0 这样的范围引用它。
  • PostCSS 的 minor 升级曾导致部分插件不兼容,表现为构建时样式输出错误——构建不报错,但页面样式不对,排查成本很高。

这就是为什么 lock 文件是生产环境的最后一道防线:你本地测试通过的版本组合,lock 文件帮你锁住了。删掉它重新安装,等于放弃了这个保障。

删 lock 重装,到底丢了什么

回到开头的问题:删掉 lock 文件再重装,你丢掉了两样东西。

第一,版本锁定。 所有依赖会按 package.json 的范围重新解析,取当前最新的可用版本。如果某个传递依赖在这段时间发了一个有问题的 patch,你就会拿到它。

第二,git 历史。 lock 文件的每次变更都有 git 记录。当你需要用 git bisect 排查"代码没改但线上表现变了"的问题时,lock 文件的 diff 是最关键的线索。删掉重建意味着这条追溯链断了。

对于一个依赖不到 50 个的小项目,这两个问题都不大——验证成本低,出了问题也容易定位。但对于依赖几百个、有完整 CI/CD 流水线的生产项目,这两个代价都不可接受。

迁移到 pnpm:三种策略,选错会出事

既然越来越多团队在迁移到 pnpm,那怎么迁才是安全的?根据项目规模,有三种策略。

策略 A:直接删 lock 重装

rm -rf node_modules package-lock.json
pnpm install

所有版本重新解析,传递依赖不可控。适合依赖少、刚起步的新项目。

策略 B:pnpm import 无损导入

pnpm import            # 从 package-lock.json 导入精确版本
rm package-lock.json   # 导入成功后删除旧 lock
pnpm install           # 安装依赖

pnpm import 会读取现有的 package-lock.json(也支持 yarn.lock),生成一个版本完全一致的 pnpm-lock.yaml。所有依赖——包括传递依赖——的精确版本都会被保留,零版本漂移。

这是大多数项目应该选择的方式。

策略 C:渐进式迁移

对于生产环境有高可用要求的项目,在策略 B 的基础上增加一个完整的验证周期:

git checkout -b chore/migrate-to-pnpm

pnpm import
rm package-lock.json
pnpm install

# 跑完所有测试
pnpm test
pnpm build
pnpm e2e

# staging 环境验证后再合入 main

怎么选

简单判断:项目依赖超过 50 个,或者跑在生产环境——用策略 B。如果还有高可用要求——用策略 C。只有刚起步的小项目才适合策略 A。

迁移后最常遇到的问题:phantom dependencies

从 npm 切到 pnpm 后,最常见的报错不是版本问题,而是 Module not found

这是因为 npm 的 flat node_modules 会把所有包平铺在根目录,你的代码可以 import 任何已安装的包,哪怕你没在 package.json 里声明。pnpm 的 symlink 结构不允许这样做。

// package.json 里没有声明 "ms"
// 但 "debug" 依赖了 "ms",npm 会把它平铺
import ms from 'ms'  // npm: 正常  |  pnpm: Module not found

修复方式很直接:把实际用到的包显式加到 package.json 里。

pnpm build 2>&1 | grep "Module not found"
pnpm add ms  # 逐个添加缺失的依赖

大项目可能需要修几十个,但这是一次性的工作,修完之后项目的依赖关系会清晰很多。

迁移后别忘了更新 CI

很多人本地迁完就提交了,CI 里还是 npm ci——然后 CI 就挂了。

GitHub Actions 的改动并不大,核心是加一个 pnpm/action-setup 步骤:

# 迁移前
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
  - run: npm ci
  - run: npm run build

# 迁移后
steps:
  - uses: actions/checkout@v4
  - uses: pnpm/action-setup@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'pnpm'
  - run: pnpm install --frozen-lockfile
  - run: pnpm build

另外建议在 package.json 里加上 packageManager 字段:

{
  "packageManager": "pnpm@10.29.2"
}

pnpm/action-setup@v4 会读取这个字段自动安装对应版本,Corepack 也会据此约束团队成员使用正确的包管理器。

lock 文件的 Git 管理:几条铁律

最后聊几个关于 lock 文件日常管理的要点。

lock 文件必须提交到 Git。 这一点怎么强调都不过分。不提交 lock 文件,团队成员的依赖版本可能各不相同,CI 构建不可复现,出了问题无法回滚到已知良好的状态。把 lock 文件加到 .gitignore 里是一个常见但严重的错误。

lock 文件冲突不要手动解。 多人开发时 lock 文件冲突是家常便饭。正确做法是接受一方的版本,然后重新生成:

git checkout --theirs pnpm-lock.yaml
pnpm install
git add pnpm-lock.yaml
git commit

pnpm install 会根据 package.json 重新解析 lock 文件,同时尽量保留已有的版本锁定。比手动合并几千行 YAML 安全得多。

CI 里永远用 --frozen-lockfile pnpm install --frozen-lockfile 等价于 npm ci,严格按 lock 文件安装。如果 lock 文件和 package.json 不一致就直接报错,而不是悄悄更新 lock 文件。

迁移 Checklist

最后附一个可以直接用的清单:

  • 确认项目能通过 build(最好有测试覆盖)
  • pnpm import 从现有 lock 文件导入
  • 删除旧 lock 文件
  • pnpm install 安装依赖
  • 修复 phantom dependency 报错
  • package.json 添加 "packageManager": "pnpm@x.x.x"
  • 更新 CI workflow
  • 全量测试 + 构建验证
  • 通知团队成员

以上就是关于 lock 文件和包管理器迁移的完整分析。核心观点只有一个:小项目随便迁,大项目用 pnpm import,别直接删 lock 文件。

你们团队在迁移包管理器或者管理 lock 文件的时候踩过什么坑?欢迎在评论区聊聊。

A股三大指数集体收涨,沪指重返3900点

36氪获悉,A股三大指数集体收涨,沪指涨1.3%重返3900点,深成指涨1.95%,创业板指涨2.01%;光模块、高速铜连接概念领涨,立讯精密、长飞光纤、铭普光磁涨停,佰维存储涨超9%;绿电概念继续走强,华电辽能8连板,晋控电力、粤电力A、湖南发展等十余股涨停;油气、煤炭继续调整,科力股份、通源石油跌超6%。

Fixed 定位的失效问题

通常情况下position: fixed元素相对于视口定位,但是某些情况下,比如祖先元素设置了transformfilterperspectivewill-change: transform的时候,子元素的固定定位会失效,不在相对于视口定位,而是相对于该祖先元素定位,约等于绝对定位。

比如:

<div style="position: fixed; top: 50vh; right: 0; width: 10vh; height: 10vh; background-color: lightblue"></div>

<div style="transform: translate(0, 0); padding-top: 25vh">
  <div style="position: fixed; top: 10vh; width: 10vh; height: 10vh; background-color: lightcoral"></div>
</div>
<div style="filter: blur(0); padding-top: 25vh">
  <div style="position: fixed; top: 10vh; width: 10vh; height: 10vh; background-color: lightcyan"></div>
</div>
<div style="perspective: 0; padding-top: 25vh">
  <div style="position: fixed; top: 10vh; width: 10vh; height: 10vh; background-color: lightgoldenrodyellow"></div>
</div>
<div style="will-change: transform; padding-top: 25vh">
  <div style="position: fixed; top: 10vh; width: 10vh; height: 10vh; background-color: lightgray"></div>
</div>

第一个元素定位正常,但后面的元素定位异常,这是因为这些元素的父元素因为特定的 CSS 属性被放在新的图层之中。

一般情况下,当我们发现了固定定位异常时,排查祖先元素是否含有上述的 CSS 属性即可。但有一种情况,虽然在浏览器的 CSS 面板中看不到上述属性,但元素依然处于不同的图层中。这就是当元素被执行过animate且执行了上述 CSS 属性的动画。

比如:

<div id="moving">
  <div style="position: fixed; top: 0; width: 10vh; height: 10vh; background-color: lightblue"></div>
</div>

<script>
  let moving = window.document.getElementById('moving');
  moving.animate([{ transform: 'translate(0, 0)' }, { transform: 'translate(0, 50vh)' }], { duration: 1000, fill: 'forwards' });
</script>

如果运行上面的代码,可以看到固定定位的元素在跟随父元素移动,同时此时看到浏览器的 CSS 面板中父元素并没有 transform 相关属性。

不得不说,好坑啊。

国家能源局:截至2月底全国累计发电装机容量39.5亿千瓦,同比增长15.9%

36氪获悉,据国家能源局消息,3月25日,国家能源局发布2026年1-2月份全国电力统计数据。截至2月底,全国累计发电装机容量39.5亿千瓦,同比增长15.9%。其中,太阳能发电装机容量12.3亿千瓦,同比增长33.2%;风电装机容量6.5亿千瓦,同比增长22.8%。1-2月份,全国发电设备累计平均利用466小时,比上年同期降低39小时。

兰亭集势:2025年净利润830万美元,实现扭亏为盈

36氪获悉,兰亭集势发布2025年第四季度及全年财报。财报显示,2025年公司实现营业收入2.24亿美元,其中第四季度营收6300万美元,同比增长9%,环比增长13.5%。公司2025年实现净利润830万美元,实现扭亏为盈,其中第四季度净利润达330万美元。截至2025年末,公司账面现金等流动资金为2602万美元,同比增长31.7%。公司董事会已延长股票回购计划至2026年6月30日,截至2026年3月13日,已回购约110万美元的ADS。

商务部:1-2月我国电子商务稳定发展,数字消费稳中向好

36氪获悉,商务部电子商务司负责人介绍2026年1-2月我国电子商务发展情况表示,2026年1-2月,我国电子商务稳定发展,数字消费稳中向好,产业电商推动数智化转型,丝路电商提升惠全球品牌效应,高质量发展实现良好开局。数字消费持续活跃。提振消费系列政策在电子商务领域落地显效。作为“购在中国”全年首场线上活动,全国网上年货节丰富节日市场消费选择。据国家统计局数据,1-2月全国网上商品和服务零售额增长9.2%。商务大数据重点监测平台智能产品增长亮眼,智能眼镜、擦窗机器人网零额分别增长183.5%和130.8%。数字技术赋能旅游、餐饮、住宿等生活性服务业,据商务大数据监测,线上预定线下体验的旅游和餐饮零售额分别增长36.1%和27.3%。

泡泡玛特:将在4月推出IP小家电产品

36氪获悉,3月25日下午,泡泡玛特召开2025年度业绩发布会。集团董事长兼CEO王宁表示,将在4月推出以IP为核心的衍生小家电产品,并在京东等电商平台发售。

金山云海南洋浦信息科技公司注册资本增至7亿美元

36氪获悉,爱企查App显示,近日,金山云(海南洋浦)信息科技有限公司发生工商变更,注册资本由6亿美元增至7亿美元,增幅约17%。该公司成立于2022年8月,法定代表人为邹涛,经营范围包括互联网域名注册服务、计算机软硬件及辅助设备批发、软件开发、软件销售等。股东信息显示,该公司由金山云有限公司全资持股。

泡泡玛特城市乐园2025年营收及客流表现超预期,二期将在2027年启动建设

36氪获悉,3月25日下午,泡泡玛特召开2025年度业绩发布会。首席运营官司德表示,当前泡泡玛特城市乐园1.5期施工正在顺利进行,预计在今年夏天与大家见面。2025年,乐园在关闭将近一大半区域的情况下,营收及客流表现均超预期。目前,乐园2期扩建已在规划中,预计于2027年启动建设,增加以SKULLPANDA和星星人为主题的更多场景。

通用汽车将向韩国子公司投资6亿美元,升级生产设施

通用汽车公司周三宣布,计划向韩国投资6亿美元,用于升级生产设施,并巩固其韩国子公司作为其小型运动型多用途车(SUV)全球枢纽的地位。通用汽车在一份新闻稿中表示,此次对通用韩国公司的投资包括去年12月宣布的3亿美元投资,以及另外3亿美元,这体现了公司对通用韩国公司业绩和竞争力不断提升的信心。(新浪财经)

MOVA割草机器人预计2026年将突破100万台

36氪获悉,近日,科技品牌MOVA旗下ViAX系列割草机器人凭借全球首创双目AI视觉技术,登顶德国、意大利、比利时、荷兰、西班牙等亚马逊多国Best Sellers榜首。截至2026年3月,MOVA割草机器人累计出货量已超30万台,同比增长500%,预计2026年将突破100万台。

React Hooks 闭包陷阱:高级场景与深度思考

前言

闭包陷阱不只是"定时器读不到最新值"那么简单。

在实际工程中,你会遇到:

  • 类组件转函数式后的隐性 bug
  • 自定义 Hook 里的闭包泄露
  • Concurrent Mode 下的闭包过期问题
  • 状态机场景下的闭包与 reducer 的相爱相杀
  • memo/useCallback 优化反而引发的新问题
  • 内存泄漏与闭包的深层关系

场景一:类组件转函数式后,ref 里的闭包成了定时炸弹

问题

你有一个类组件,习惯用 this 解决问题:

// 类组件写法
class SearchPanel extends React.Component {
  state = { keyword: '' };

  handleSearch = () => {
    // 这里直接用 this.state.keyword,永远是最新的
    api.search(this.state.keyword);
  };

  render() {
    return <input onChange={e => this.setState({ keyword: e.target.value })} />;
  }
}

改成函数式后,你可能这样写:

// ❌ 常见错误写法
function SearchPanel() {
  const [keyword, setKeyword] = useState('');

  const handleSearch = () => {
    // 等等,这里怎么获取 keyword?
    // 很多人会想到用一个 ref 存着
  };

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={handleSearch}>搜索</button>
    </div>
  );
}

然后你用 ref 来"绕过"闭包问题:

// ❌ 潜在问题
function SearchPanel() {
  const [keyword, setKeyword] = useState('');
  const keywordRef = useRef(keyword);

  // 同步 ref
  useEffect(() => {
    keywordRef.current = keyword;
  }, [keyword]);

  const handleSearch = () => {
    // 用 ref 获取值
    api.search(keywordRef.current); // ⚠️ 这里看起来没问题
  };

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={handleSearch}>搜索</button>
    </div>
  );
}

问题在哪?

如果用户快速点击搜索按钮(比点击一次还快),在 useEffect 还没执行之前,ref 里还是旧值。

进阶视角

类组件的 this.state 本质上是一个"永远指向最新值"的 mutable 对象。函数式的 useState 是 immutable 的,每次渲染都是新值。

正确的函数式写法:

// ✅ 正确:不要绕过 React 的响应式系统
function SearchPanel() {
  const [keyword, setKeyword] = useState('');

  // 直接把当前值传进去,不要通过 ref 间接获取
  const handleSearch = () => {
    api.search(keyword); // ✅ 这里就是最新的 keyword
  };

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={handleSearch}>搜索</button>
    </div>
  );
}

架构思考:

类组件 函数式组件
this.state 是 mutable,引用永远最新 useState 是 immutable,每次渲染是新值
闭包不是问题,因为用的是 this 闭包是问题,因为捕获的是旧值
解决方案:忘了它,用响应式数据 解决方案:让函数组件在正确的时机重新创建

场景二:自定义 Hook 里的闭包泄露——你封装的 Hook 可能正在泄露内存

问题

你封装了一个 useInterval Hook:

// ❌ 有问题的 useInterval 实现
function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const id = setInterval(() => {
        savedCallback.current(); // 这里调用的是最新的 callback
      }, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

看起来没问题?好,我们来用一下:

function MyComponent() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    console.log('count:', count); // ⚠️ 这里永远打印 0
  }, 1000);

  return <div>{count}</div>;
}

这不就是场景一的问题吗?

但更严重的问题在后面:

如果 callback 每次渲染都变化(比如用了一些依赖),savedCallback.current 会不断更新,但旧的 callback 形成的闭包可能被某些地方持有,导致内存无法释放。

源码级分析

// 模拟问题场景
function useDataFetcher(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(url)
      .then(res => res.json())
      .then(result => {
        if (!cancelled) {
          setData(result); // ⚠️ 这个闭包捕获了 url
        }
      });

    return () => {
      cancelled = true; // 这里的逻辑其实有漏洞
    };
  }, [url]); // url 变化 → 新的 effect → 新的闭包

  return data;
}

问题:url 快速变化时(比如搜索框输入),旧请求的回调虽然检查了 cancelled,但闭包本身还在内存中。如果这个闭包捕获了大数据(比如列表数据),就有内存泄漏风险。

高级视角:正确的 useInterval 实现

// ✅ 正确的 useInterval(借鉴 ahooks)
import { useEffect, useRef, useCallback } from 'react';

function useInterval(callback, delay) {
  const callbackRef = useRef(callback);

  // 每次 callback 变化,同步更新 ref
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // 定时器执行时,永远读 ref 里的最新函数
  useEffect(() => {
    if (delay === null || delay === undefined) {
      return;
    }

    const tick = () => callbackRef.current();

    const id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]); // 注意:这里不依赖 callback,只依赖 delay
}

但真正的架构问题是:

你的自定义 Hook 使用者,可能根本不知道内部有闭包陷阱。他们传入的 callback 如果依赖了外部变量,问题就会隐藏在这里。

最佳实践:

// ✅ 在自定义 Hook 里用 useLatest 统一处理
function useLatest(value) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}

function useInterval(callback, delay) {
  const callbackRef = useLatest(callback);

  useEffect(() => {
    if (delay == null) return;

    const tick = () => callbackRef.current();
    const id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

场景三:useReducer 里的闭包——状态机场景的特殊情况

问题

你可能觉得用了 useReducer 就不用管闭包了:

// ❌ 仍然有闭包问题
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  useEffect(() => {
    const timer = setInterval(() => {
      // ❌ 这里还是闭包陷阱!
      dispatch({ type: 'INCREMENT' });
      // 等等,dispatch 需要读旧状态吗?
      // 让我们看看
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  return <div>{state.count}</div>;
}

实际上这个例子可以跑,因为 dispatch 的工作方式不同。

源码级解析:dispatch 为什么特殊?

// ReactFiberHooks.js
function updateReducer(reducer, initialArg, init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.memoizedQueue;
  const pending = queue.pending;

  // 关键:dispatch 不依赖任何外部变量
  // 它的行为是"把 action 放入队列",不是"立即执行"
  // 所以 dispatch 本身不会过期

  if (pending !== null) {
    // ...
  }

  const newState = hook.memoizedState;
  return [newState, dispatch];
}

所以:

操作 是否受闭包影响
setCount(n) ❌ 不受(但 n 可能是旧值)
setCount(c => c + 1) ✅ 不受,函数式更新
dispatch({ type: 'INCREMENT' }) ✅ 不受,dispatch 只是发指令

真正的问题:reducer 里的闭包

// ❌ 问题在 reducer 内部
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT_BY':
      // 这里需要访问外部的某个"配置"
      return { count: state.count + action.amount };
    default:
      return state;
  }
}

function Counter({ defaultAmount = 1 }) { // ⚠️ props 变化
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  // 这里的 defaultAmount 变化时,reducer 不会自动更新
  // 你需要确保 action 携带足够的信息
  const handleIncrement = () => {
    dispatch({ type: 'INCREMENT_BY', amount: defaultAmount });
  };

  return <button onClick={handleIncrement}>{state.count}</button>;
}

高级视角:

useReducer 并不是闭包的银弹。它的优势是把"如何计算新状态"和"何时触发计算"分开,但如果你在 reducer 外部依赖了某些值,闭包问题依然存在。


场景四:memo 与 useCallback——优化反而引发的新问题

问题

用了 memo + useCallback 做性能优化,结果闭包问题更严重了:

// ❌ 过度优化的陷阱
const Child = memo(function Child({ onClick, data }) {
  console.log('Child 渲染了');
  return <button onClick={() => onClick(data.id)}>{data.label}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([{ id: 1, label: 'A' }]);

  // ❌ 用 useCallback 包裹,但依赖了 list
  const handleClick = useCallback((id) => {
    console.log('点击了', id, list); // ⚠️ 这里永远是旧 list
  }, [list]); // list 变化 → handleClick 重建 → Child 重新渲染

  return (
    <div>
      <Child onClick={handleClick} data={list[0]} />
      <button onClick={() => setCount(c => c + 1)}>count: {count}</button>
    </div>
  );
}

useCallback 是想避免子组件重渲染,结果因为依赖了 listlist 每次变化 handleClick 都会重建,子组件还是重渲染了。

什么时候真正需要 useCallback?

// ✅ 正确的用法:传给子组件的回调
function Parent() {
  const [count, setCount] = useState(0);

  // 只有当这个函数要传给 memo 过的子组件时,才用 useCallback
  const handleClick = useCallback(() => {
    console.log(count); // 如果需要读 count,加依赖
  }, [count]);

  return (
    <div>
      <MemoChild onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>count</button>
    </div>
  );
}

// ✅ 另一种思路:用 useRef 存最新值,不让子组件依赖变化
function Parent() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClick = useCallback(() => {
    console.log(countRef.current); // ✅ 不依赖变化,函数永远不重建
  }, []); // 空依赖,永远是同一个函数

  return (
    <div>
      <MemoChild onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>count</button>
    </div>
  );
}

架构决策:

场景 推荐方案
回调需要读最新 state 加依赖,或用 ref
回调只需要"触发动作" useCallback + 空依赖
子组件是 memo 的 优先确保 props 不变
性能问题根源不在这里 先用 React DevTools Profiler 定位

场景五:Concurrent Mode 下的闭包过期——时间切片带来的新问题

问题

React 18 开启了 Concurrent Mode,同一个组件可能同时存在多个版本的渲染。这让闭包问题更复杂了:

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 发起搜索请求
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        // ⚠️ 关键问题:这里拿到的 query 是哪个版本的?
        setResults(data);
      });

    return () => controller.abort();
  }, [query]);

  return <div>{results.map(r => <li key={r.id}>{r.title}</li>)}</div>;
}

在 Concurrent Mode 下,可能发生这种情况:

  1. 用户输入 "a",React 开始渲染 "a" 的搜索结果
  2. 用户快速输入 "ab",React 中断 "a" 的渲染,开始渲染 "ab"
  3. "ab" 的请求先返回,设置 results = ["ab 结果"]
  4. "a" 的请求后返回,设置 results = ["a 结果"]

结果:用户看到了"ab"的搜索框,却显示着"a"的结果。

源码级分析:React 18 的 thenable 机制

// ReactFiberCommitWork.js
function commitEffect() {
  // ...
  if (thenableState !== null) {
    // 异步更新可能会被 "插队"
    // 这里的状态更新不是线性的
  }
}

如何应对 Concurrent Mode 的闭包?

// ✅ 方案一:使用 AbortController 取消旧请求
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => {
      // ✅ 再次检查 query 是否还是当前值
      setQuery(current => {
        if (current !== query) return current; // 如果已经变了,忽略这次更新
        return current;
      });
      setResults(data);
    });

  return () => controller.abort();
}, [query]);

// ✅ 方案二:使用 useDeferredValue(React 18)
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  // 用 deferredQuery 做渲染,用 query 做请求
  // 渲染可以是"过期"的,但数据请求是最新的
}

// ✅ 方案三:使用 useSyncExternalStore( React 18 官方方案)
import { useSyncExternalStore } from 'react';

// 自己管理订阅,确保读取到的是"已提交的"值
function useSearchQuery(query) {
  const snapshot = useSyncExternalStore(
    subscribe,
    getServerSnapshot,
    getClientSnapshot(query)
  );
  return snapshot;
}

场景六:异步函数在 useEffect 里的闭包——最常见的内存泄漏

问题

这是一个经典但容易被忽视的问题:

// ❌ 内存泄漏的典型案例
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let isMounted = true;

    fetchUser(userId).then(user => {
      if (isMounted) {
        setUser(user); // ⚠️ 如果组件已卸载,这里仍然会执行
      }
    });

    return () => {
      isMounted = false; // 这是一个闭包,但它不是过期闭包的锅
    };
  }, [userId]);

  if (!user) return <Loading />;

  return <div>{user.name}</div>;
}

等等,这个例子其实是正确的写法(加了 isMounted 标记)。

真正的问题在下面:

// ❌ 真正的内存泄漏
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const subscription = userService.subscribe(userId, (newUser) => {
      setUser(newUser); // ⚠️ 组件卸载时没有取消订阅!
    });

    return () => {
      // ❌ 忘记取消订阅
      // subscription.unsubscribe();
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

闭包与内存泄漏的关系

问题类型 闭包的角色 解决方案
过期闭包读旧值 闭包捕获旧变量 用 ref / 函数式更新
异步完成后 setState 组件已卸载 用 isMounted 或 AbortController
事件订阅未清理 闭包持有组件引用 useEffect 返回清理函数
定时器未清理 闭包持有组件引用 clearInterval / clearTimeout

一个更隐蔽的例子:

// ❌ 定时器 + 闭包 = 内存泄漏
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(s => s + 1); // ✅ 函数式更新,没问题
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>{seconds}</div>;
}

// 但如果这样写:
function TimerWithBug() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // ❌ 没有返回清理函数
    const timer = setInterval(() => {
      setSeconds(seconds + 1); // 读的是闭包里的 seconds,永远是 0
    }, 1000);
    // 组件卸载时 timer 还在运行 → 内存泄漏
  }, []); // 依赖数组为空,effect 不重新执行,所以也不会修复

  return <div>{seconds}</div>;
}

场景七:Server Components 下的闭包差异—— React 19 的新挑战

⚠️ React 19 / Next.js App Router 场景

问题

Server Components (RSC) 和 Client Components 的闭包行为完全不同:

// ❌ Server Component(默认)
async function Profile({ userId }) {
  const user = await fetchUser(userId); // ✅ 直接 await,不需要 useEffect

  // 这个函数组件在服务端渲染,不会创建闭包
  // 因为它只执行一次,返回 JSX
  return <div>{user.name}</div>;
}

// ✅ Client Component
'use client';
function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user?.name}</div>;
}

架构差异:

特性 Server Components Client Components
闭包问题 无(只渲染一次) 有(每次渲染可能创建闭包)
数据获取 直接 async/await useEffect + 依赖数组
状态管理 无状态 useState/useReducer
包体积 不打包到客户端 打包到客户端

如何设计?

原则:尽量把不需要交互的组件写成 Server Components。

// ✅ 正确的分层
// ProfilePage.tsx (Server Component - 默认)
import Profile from './Profile';

export default async function ProfilePage({ params }) {
  // 服务端获取数据
  const user = await fetchUser(params.userId);

  // 只把需要交互的部分交给客户端
  return (
    <main>
      <h1>{user.name}</h1>
      <Profile initialUser={user} />
    </main>
  );
}

// Profile.tsx ('use client')
'use client';
function Profile({ initialUser }) {
  const [user, setUser] = useState(initialUser); // 用 initialUser 初始化

  // 只有这里的交互逻辑才需要处理闭包
  return <EditableUser user={user} onSave={setUser} />;
}

场景八:微前端场景下的闭包——qiankun / single-spa 下的特殊问题

问题

在微前端架构中,主应用和子应用各自有独立的 React 实例。闭包问题可能跨应用传播:

// 主应用
function MainApp() {
  const [user, setUser] = useState(null);

  // 传递给子应用的回调
  const handleUserUpdate = useCallback((newUser) => {
    setUser(newUser);
  }, []);

  return (
    <div>
      <MicroApp
        name="user-profile"
        onUserUpdate={handleUserUpdate}
      />
    </div>
  );
}

// 子应用(独立 React 实例)
function UserProfile({ onUserUpdate }) {
  const [user, setUser] = useState({ name: 'Tom' });

  useEffect(() => {
    // ⚠️ onUserUpdate 是从主应用传过来的
    // 它的闭包是在主应用的渲染周期里创建的
    // 子应用的状态变化,可能触发主应用的更新
    onUserUpdate(user);
  }, [user, onUserUpdate]);

  return <div>{user.name}</div>;
}

微前端下的闭包治理

// ✅ 方案:使用事件总线或状态管理,不直接传回调
// eventBus.js
import mitt from 'mitt';
export const bus = mitt();

// 主应用
function MainApp() {
  useEffect(() => {
    bus.on('user-update', (user) => {
      setUser(user);
    });
    return () => bus.off('user-update');
  }, []);

  return <MicroApp name="user-profile" />;
}

// 子应用
function UserProfile() {
  const [user, setUser] = useState({ name: 'Tom' });

  useEffect(() => {
    bus.emit('user-update', user);
  }, [user]);

  return <div>{user.name}</div>;
}

为什么这样更好:

  1. 解耦:子应用不需要知道谁在监听
  2. 最新值:事件触发时读取的是当前值,不存在闭包捕获旧值
  3. 可清理:在 useEffect 返回的函数里可以取消监听

总结:闭包问题的本质与架构思考

闭包问题的本质

JavaScript 闭包 = 函数 + 作用域链
React 函数式组件 = 每次渲染 = 新的函数 + 新的作用域

两者结合 = 每次渲染创建新闭包,可能捕获旧值

高级视角的解决思路

层级 策略 工具
代码规范 exhaustive-deps 强制检查 eslint-plugin-react-hooks
组件设计 避免深层传递 callbacks Context / 状态管理
抽象封装 自定义 Hook 统一处理 useLatest / useInterval
架构分层 Server vs Client 分离 RSC / 'use client'
运行时 Concurrent Mode 适配 useDeferredValue / useSyncExternalStore
微前端 跨应用通信用事件总线 mitt / postMessage

最后一句

闭包不是 bug,是 JavaScript 的核心特性。React 用函数式范式重新定义了 UI,闭包问题只是这条路上的"学费"。

欢迎关注公众号程序员蜡笔熊,欢迎点赞转发,有什么意见或指正欢迎评论区评论。

❌