普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月9日首页

早报|雷军:小米将会在今年推出一款重磅产品/「全球首款」可量产全固态电池遭质疑/Google逆袭成功,母公司市值超苹果

作者 柯铭源
2026年1月9日 09:23
cover

🤖

何小鹏:今年规模量产人形机器人

🏆

小米 2025 千万技术大奖颁发

💵

Google 系市值 7 年来首次超过苹果

🚗

贾跃亭携新车亮相 CES:很快回国

☁

被小米辞退 4 个月后,王腾官宣成立新公司

🔩

阿里云发布多模态交互开发套件

🔋

「全球首款」可量产全固态电池遭质疑

🗣

阶跃星辰端到端语音模型完成全球首秀

🦎

京东回应成立「变色龙业务部」

💡

智谱 CEO:实现 AGI 是终极目标

📷

荣耀 Magic8 Pro Air 曝光

😯

可以科技发布全球首款桌面 AI 伙伴

🔥

一加 Turbo 6 系列正式发布

🥢

海底捞「小便门」当事人登报道歉

📱

迪士尼或入局竖屏视频内容

重磅

何小鹏:今年规模量产人形机器人

昨日,小鹏汽车举行「2026 小鹏全球新品发布会」。

会上,何小鹏透露,2026 年,小鹏汽车将迎来物理 AI 的落地与规模量产,正式开启从技术探索向实践应用的关键跨越:

2026 年第一季度第二代 VLA 上车;2026 年开启运营 Robotaxi;2026 年规模量产人形机器人;2026 年规模量产飞行汽车。

在去年 11 月,小鹏在科技日活动上展示了最新一代人形机器人 IRON。该机器人在舞台上走「猫步」如人一样轻盈,因此引发互联网一阵猜疑,不少网友更是称「里面是真人扮演的」。

活动后,何小鹏及机器人团队不得不将机器人蒙皮、打印骨骼剪开,以此证明「真·机器人」。

本次「2026 小鹏全球新品发布会」上,小鹏还发布了多款纯电/增程车型。

🔗 相关阅读:既要又要还要?小鹏四车同发,P7+ 超级增程版 18.68 万元起

据悉,本次发布会为小鹏产品线新增了超级增程版的 P7+ 和 G7,以及换装图灵 AI 芯片的纯电 G6 和 G9。

四款新车均提供 Ultra SE(2 颗图灵 AI 芯片+第二代 VLA 模型)/Ultra(3 颗图灵 AI 芯片+第二代 VLA 和 VLM 大模型)两个版本,售价分别为 12000 元和 20000 元。

  • 26 款 P7+:增程纯电同价,均为 18.68 万元起。

新车尾灯改为全新一体星轨式设计,车身加长 15mm;车内空间方面,新款 P7+通过采用厚度仅 109mm 的 800V 电池包以及双层悬浮尾翼设计,将后排有效头部空间提升至 973mm。

增程车型采「49.2kWh 的 5C 超充 AI 电池配合 45L 油箱(增程器支持 92 号)」组合,CLTC 纯电续航里程达到 430 公里。

P7+ 全系标配了前双叉臂、后五连杆独立悬架,并辅以 CDC 连续阻尼控制系统及太极液压衬套矩阵;新增新款仪表盘和 HUD。另外,新车还提供前后排静音电吸门、RNC+ENC 主动降噪技术及前排双层夹胶玻璃。

  • 26 款 G7:增程纯电同价,均为 19.58 万元起。

新车搭载了「鲲鹏」超级增程系统,由 55.8kWh 的 5C 超充 AI 电池与 60L 大容积油箱组成。据介绍,增程版 G7 CLTC 综合续航达到了 1704 公里,纯电表现同样拥有 430 公里表现。

小鹏针对增程车型常见的「亏电性能衰减」问题进行了专项优化。即便在低电量状态下,动力输出依然保持稳定,可维持 150km/h 的高速巡航。

底盘动态与行驶质感上,G7 全系标配前双叉臂、后五连杆独立悬架,并集成了 CDC 连续阻尼控制系统与太极液压衬套。主动安全方面,新车系统支持最高 130km/h 的 AEB 自动紧急刹车,并集成了冰雪路面 AES 紧急避让功能。

  • 新款 G6、G9

2026 款小鹏 G6 与 G9 均搭载小鹏自研的「3 图灵芯」架构,三颗图灵芯片分别专用于座舱交互、智能驾驶感知融合与整车域控制(含电池、底盘),实现高算力、低延迟、强协同的全车 AI 闭环。

除此之外,新款 G6 新增新昼灰车漆、无边框外后视镜,内饰取消扶摇绿,推出气宇灰、深空灰、秘境蓝三色;前排坐垫加长至 514mm 标配加热通风;新增 OMS 乘员监控与 INC 主动降噪+双层隔音玻璃。

新款 G9 新增 P7 同款新月银车漆,全系标配流媒体后视镜、OMS 系统。虽未推出增程版,但凭借双腔空悬、800V 平台和扎实底盘,驾控与舒适依旧能打。

大公司

小米 2025 千万技术大奖颁发

1 月 7 日,2025 小米「千万技术大奖」颁奖典礼正式举办。

据介绍,小米自研芯片「玄戒 O1」凭借创新性、领先性和影响力等多个维度的卓越表现,荣获千万技术大奖最高奖项。同时,小米集团创始人、董事长兼 CEO 雷军连续七年出席颁奖典礼并给获奖团队颁奖。

雷军表示,「玄戒 O1 获得这次千万技术大奖的最高荣誉,当之无愧。玄戒 O1 发布到现在,用户、媒体的口碑、评价都非常不错,我们这支芯片队伍非常争气,希望未来再接再厉,尽早拿出更好的作品。」

据悉,玄戒 O1 由小米自主研发设计,采用第二代 3nm 工艺制程,创新十核四丛集架构,兼顾强大性能与日常能效,回片仅 6 天便打通手机全功能,性能体验跻身全球第一梯队。

值得关注的是,雷军在典礼上提到:

2026 年,小米预计将在一款终端上实现自研芯片、自研 OS、自研 AI 大模型「大会师」,同时,积极推动机器人业务的创新发展。

另外,同时获奖的还有小米 17 Pro 系列-妙享背屏、2200MPa 小米超强钢、小米汽车四合一域控制模块以及小米智能眼镜创新架构、端到端+强化学习寻位泊车辅助系统、1000 万 Clips 版小米端到端辅助驾驶系统、小米超级像素、LOFIC 高动态影像技术、异形高硅电池结构技术、有序介孔硅碳电池材料等 10 个项目。

Google 系市值 7 年来首次超过苹果

昨日,Google 母公司 Alphabet 股价逆市上涨 2.5%,市值攀升至 3.89 万亿美元,这是自 2019 年以来,其市值首次超过苹果。

🔗 相关阅读:干翻 ChatGPT,市值超苹果,这就是 AI 圈最大爽文

究其原因,根据外媒多篇报道,我们也可以拆解出 Google 的 AI 三板斧:技术打底、资源合并、商业落地。三步环环相扣,构成了 Google AI 战略的完整闭环。

值得一提的是,2025 年,图片生成模型领域出现了两次病毒式传播事件:一次是 ChatGPT 的 AI 吉卜力画风走红全网,另一次则是 Google 的 Nano Banana(Gemini 2.5 Flash Image)。

这款名字独特的产品,很快引发了市场热潮。短短数天内,Nano Banana 就登顶 LMArena 平台性能排行榜,在社交网络上广泛传播,其影响力远超 Google 内部的预期。

而在去年 9 月,搭载 Nano Banana 的 Gemini AI 应用成功登顶苹果 App Store 下载榜。

登顶 2 个月后,Google 顺势推出了迄今为止最强版本的 Gemini 3 系列模型,其多项核心指标超越 ChatGPT,也让 OpenAI CEO 奥特曼紧急拉响「红色警报」,亲自下令改进 ChatGPT。

而根据昨天 SimilarWeb 发布的最新数据,Gemini 全球网页端流量份额首次突破 20% 的市场份额,而 ChatGPT 的份额从 2025 年 1 月 86% 暴跌至 64.5%。

贾跃亭携新车亮相 CES:很快回国

据网易科技消息,在拉斯维加斯举行的 CES 2026 上,Faraday Future(法拉第未来)创始人贾跃亭携 MPV 新车 FX Super One 亮相 CES。

据悉,FX Super One 预计今年量产,售价未定。当被问到何时回北京时,贾跃亭说:「很快很快」。

据悉,2025 年底,法拉第未来(Faraday Future, FF)在美国共举办了两场发布会,分别聚焦于 FF 91 2.0 和全新子品牌 FX 的首款车型 FX Super One。FX Super One 对标凯迪拉克凯雷德,强调「空间+AI+舒适」的全面超越。

值得一提的是,FX Super One 前格栅处拥有一块类似电视的大屏幕。

据此前消息,法拉第未来的核心逻辑是,利用 FF 的美国车企身份,将中国车企(如长城)生产的整车或 KD 件运至美国,在 FF 工厂完成最后组装+贴标。

被小米辞退 4 个月后,王腾官宣成立新公司

2025 年 9 月,前小米中国区市场部总经理王腾因泄密而被辞退。

而在昨日,王腾发文宣布,其从小米离开后开始筹备创业,而新公司近期已经成立,并取名为「今日宜休」。

王腾介绍,「今日宜休」目标是通过研发睡眠健康相关的产品,让大家能拥有更好的精力状态。

同时其还解释了为什么选择睡眠健康、精力管理这个方向:

  • 睡眠、精力已经成为每个人都关心的健康问题。
  • 社会对睡眠的价值理解有待提升。
  • 新时代下 AI 大模型发展迅速,让很多产品的体验能大幅提升。

值得关注的是,王腾强调,公司目前已经组了一个初创团队,核心成员主要来自小米、华为等头部科技大厂,具备丰富的软硬件产品开发经验。

王腾还表示,目前公司还在重点招聘「软硬件产品经理」「健康/AI 算法工程师」「ID&CMF 设计师(智能穿戴/家居方向)」等方向的岗位。

而博主「数码闲聊站」在评论区询问「(产品)有兴趣接入米家生态吗」时,王腾表示「接,必须接」。

阿里云发布多模态交互开发套件

1 月 8 日,在阿里云通义智能硬件展上,阿里云发布多模态交互开发套件,该套件集成了千问、万相、百聆三款通义基础大模型,并预置十多款生活休闲、工作效率等领域的 Agent 和 MCP 工具。

据介绍,阿里云多模态交互开发套件为硬件企业和解决方案商提供了低开发门槛、响应速度快、场景丰富的平台

  • 在芯片层面,该套件适配了 30 多款主流 ARM、RISC-V 和 MIPS 架构终端芯片平台,满足市面上绝大多数硬件设备的快速接入需求。
  • 未来,通义大模型还将与玄铁 RISC-V 实现软硬全链路的协同优化,实现通义大模型家族在 RISC-V 架构上的极致高效部署和推理性能。
  • 在模型优化层面,除通义模型家族外,阿里云还针对大量多模态交互场景进行分析,推出适合 AI 硬件交互的专有模型,全面支持全双工语音、视频、图文等交互方式,端到端语音交互时延低至 1 秒,视频交互时延低至 1.5 秒。
  • 此外,该套件预置十多款 MCP 工具和 Agent,覆盖生活、工作、娱乐、教育等多个场景,例如,基于预置的出行规划 Agent,用户可直接调用路线规划、旅行攻略、吃喝玩乐探索等能力。
  • 该套件还接入了阿里云百炼平台生态,用户不仅可以添加其他开发者提供的 MCP 和 Agent 模板,还能通过 A2A 协议兼容三方 Agent。

现场,阿里云还展示了面向智能穿戴设备、陪伴机器人、具身智能等领域的解决方案。

「全球首款」可量产全固态电池遭质疑

日前,源于芬兰、总部在爱沙尼亚的 Donut Lab 公司在 CES 2026 上推出了号称「全球首款可量产、可应用于实际车辆的全固态电池」。

根据 Donut Lab 官方发布的数据,这款固态电池的能量密度为 400 瓦时/公斤,支持 5 分钟内充满电,并允许反复完全放电而不会衰减——它在高达 10 万次的充电循环后电池容量衰减极小,并且在零下 30 摄氏度至 100 摄氏度以上的温度范围内仍能保持 99% 以上的容量。

此外,这种固态电池由于不含易燃的液态电解质,消除了热失控、枝晶形成风险,即便电池发生损坏,也不会起火燃烧。并且,这款电池已经装配在 Verge Motorcycles 的电动摩托车上,并计划在今年开始交付。

但消息公布之后,目前关于这款全固态电池的所有信息主要来自 Donut Lab 自身发布和媒体转载报道,至今没有公开的权威第三方机构发布的测试报告证实 Donut Lab 电池确实在广泛条件下实现了上述指标。

此外,公开资料显示,这家公司直到 2024 年才从 Verge Motorcycles 分拆出来,关于该公司 CEO Marko Lehtimäki 也未有相关科研的详细背景。

而据第一财经日前采访 Donut Lab 员工,公司此前一直处于保密研发状态,几天前才突然公开成果,打了全世界一个措手不及。

针对电池材料问题,该员工表示电池的核心材料以及生产工艺不便透露,也不会把具体的技术细节写在论文里公布,因为这是公司的专有技术。

我可以透露的是,这款电池不含稀土元素,也没有使用锂。它采用的是一种和传统全固态电池截然不同的技术路线。

该名员工还表示,公司目前的产能约为 1 吉瓦时,计划明年年初将产能提升到 20-30 吉瓦时。

当被问及「5 分钟充满」「10 万次循环」等关键数据是否有第三方验证时,一位工作人员起初表示不确定,并解释「5 分钟充电」是在理想条件下,目前用 100 千瓦充电桩实际需要 10 分钟左右。

随后,另一位正与车企交流的工作人员补充道,电池正在接受外部独立测试,预计两周后会公开测试结果。

他也提到,在今年交付摩托车后,一定会有竞争对手拆解研究,届时技术细节就会被曝光,公司有半年左右的先发优势,必须利用这段时间抢占市场。Donut Lab 的这项技术受到专利保护,竞争对手想要复制还需要很长时间。

MiniMax 将创下近年来港股 IPO 机构认购历史记录

据腾讯财经报道,即将于今日(1 月 9 日)敲钟上市的大模型公司 MiniMax,创下近年来港股 IPO 机构认购历史记录。此次参与 MiniMaxIPO 认购的机构超过 460 家,超额认购达 70 多倍。

此前的认购记录属于宁德时代,其在 2025 年登陆港股市场时,剔除基石后超额认购 30 倍。

与此同时,此次参与 MiniMax 国配订单的需求达到了 320 亿美元,最后超过 460 家机构实际下单了 190 亿美元。剔除基石部分,此次 MiniMax 的国配认购超额到 79 倍左右。

此次 MiniMax 备受头部基金的青睐。这些下单的机构中不乏众多的长线基金及国家主权基金。包括新加坡主权基金、南非以及中东、加拿大等多家主权基金认购金额超过 10 亿美金。

与此同时,这些长线基金的认购订单总额超过 60 亿美金。

除了国配部分外,一些外资长线基金及国家主权基金也参与了 MiniMax 的基石认购部分。公开数据显示,MiniMax 的 14 家基石,其中包括了中东国家主权基金阿布扎比基金、韩国长线基金未来资产等。

据悉,1 月 8 日下午的暗盘显示,MiniMax 开盘后一路上涨,最高曾达到 211.2 港元每股,最低也曾达到 180 港元每股,最后收盘价为 205.6 港元每股,涨幅 24.6%。

MiniMax 的收入来源主要有两部分,AI 原生产品、开放平台及其他基于 AI 的企业服务,其中 AI 原始产品包括大语言模型、视频生成模型等于 2025 年 6 月底的收入达到 3802 万美元,占比超过 70%。

值得一提的是,截至 2025 年 9 月底,AI 原生产品累计用户达 2.12 亿,其中付费用户超过 177.1 万。

阶跃星辰端到端语音模型完成全球首秀

日前,在 2026 年国际消费类电子产品展览会(CES 2026)现场,吉利银河 M9 搭载阶跃星辰端到端语音大模型惊艳亮相。

据悉,去年这款旗舰级 SUV 曾凭借极致智能化和性价比在中国市场走红。在 CES 2026,它以「最具活人感」的语音交互体验,令外国观众直呼「Unbelievable」。

据了解,吉利银河 M9 智能座舱的语音交互系统,搭载了阶跃星辰端到端语音模型 Step-Audio 2,具备深度情感共鸣能力和个性化记忆能力,解决了传统车载 AI 语音(即 ASR-NLP-TTS 串联架构)的天然缺陷,如因信息损耗导致不智能、机械感重、时延长等问题。

在交互演示中,吉利银河 M9 展现出了超高的情商与智商,标志着智驾座舱已经从纯工具属性进化为具备逻辑思辨能力的「数字伴侣」。

此外,吉利银河 M9 语音交互系统还支持一键切换声音风格、自定义语速、音色等个性化元素。

值得关注的是,这套语音系统还展现了强大的长短期记忆能力,基于阶跃星辰模型能力的支持,AI 助手能够记住用户的个性化偏好、性别、年龄等信息,持续进化成越来越「懂你」的伙伴。

京东回应成立「变色龙业务部」

据新浪科技报道,昨日有消息称,京东成立「变色龙业务部」,全面承接 JoyAI App、JoyInside、数字人等核心 AI 产品的打造与商业化。

对于上述消息,京东方面回应表示,「AI 技术商业化加速落地京东变色龙业务部成立,第二批自研 AI 玩具将于 1 月中旬全面上线。」

京东还表示,此次变色龙业务部的成立,是将京东此前沉淀的前沿技术能力,系统性地向产业端输送的关键举措,也将有助于整合技术、产品、市场与销售资源,提升对市场需求的响应速度与商业化效率,为 AI 业务的长期发展注入更强活力。

💡 智谱 CEO:实现 AGI 是终极目标

智谱 CEO 张鹏(左一)

1 月 8 日,北京智谱华章科技股份有限公司(02513.HK)(「智谱」)正式在香港联合交易所挂牌上市。而这意味着,全球首家以通用人工智能(AGI)基座模型为核心业务的上市公司花落中国。

而智谱 CEO 张鹏日前在接受第一财经采访时表示:如果舍弃基座模型而一味选择调用,舍弃 AGI 探索而一味追求商业化变现,公司对他来讲,将丧失意义。

据悉,大模型赛道自 2022 年底被 ChatGPT 引爆后,如今已迈入第四个年头。国内市场中,大模型参赛选手正经历一轮剧烈的「适者生存」筛选。

对于高额投入问题,张鹏回应称,智谱要走一条探索性路径,这条路成本高、风险高,但总要有人走。

2025 年 DeepSeek 爆火出圈,行业有声音认为中国 AI 彻底追上美国,张鹏认为这种观点过于乐观,双方模型发布也只是阶段性状态,冰山之下有太多外界看不到的研发投入与创新积累。

其表示,目前整个大模型行业的训练路线还处于一个不确定状态,因此高额投入还会持续很长一段时间。

针对市场局面,张鹏称,大厂具备资源更多、触角更深、覆盖面更广等优势,而智谱的唯一目标是 AGI,这与上一代仅解决单一场景(如人脸识别)的人工智能截然不同:

它是通用智能各项能力的有机集合,核心要突破自我学习难题。

对此,张鹏表示,智谱绝不会放弃 AGI 拼图上的任何重要环节,包括多模态、代码、推理等能力,都是构成完整智能的关键拼图,缺一不可,这些领域都会持续深耕。

新产品

荣耀 Magic8 Pro Air 曝光

昨日,荣耀 Magic8 系列的两款新机外观信息遭到曝光。

从报告信息显示,荣耀 Magic8 Pro Air 采用了横向跑道贯穿式镜头模组,并且由三枚镜头组成,其中左侧单颗摆放,闪光灯在模组之外。从细节显示,新机或采用一体冷雕后盖。

新机将提供橙色、紫色、白色、黑色四款配色,配备直边金属中框。据此前消息透露,新机厚度为 6.3mm、重量为 158g,配备 1/1.3 英寸大底主摄。

而据博主「数码闲聊站」透露,荣耀 Magic8 RSR 方面将采用不规则类圆形镜头模组,配备后置三摄,拥有 2 亿像素 85mm f2.6 潜望长焦,主摄光圈为 f1.6。

可以科技发布全球首款桌面 AI 伙伴

近期,在在全球消费电子展(CES)开幕之际,中国机器人创新企业可以科技正式发布其战略新品 DeskMate。

据介绍,作为全球首款融合情感交互与办公助理功能的桌面机器人,DeskMate 通过多模态 AI 感知与实时情感计算,实现了「无需下令,主动理解」的自然交互,旨在重新定义下一代人机协作新范式。

DeskMate 能通过摄像头与传感器实时捕捉用户表情、动作与语音语调,经本地多模态模型综合解析,动态生成相应的表情反馈。

而这意味着 DeskMate 的每一次回应都是「实时创作」,而非调用预制动作库,从而彻底摆脱了传统机器人交互中的机械感与重复感,实现了近乎真实的「生命感」交互。

在办公效率层面,DeskMate 深度集成在用户的工作环境中。它可以通过安全连接实时同步电脑屏幕内容与剪贴板,并接入邮箱、日历、即时通讯等主流办公软件。

除了提升效率,DeskMate 还被赋予重要的情感陪伴价值——它能识别用户的工作状态。

图自 9To5Mac

值得一提的是,据彭博社 Mark Gurman 此前爆料,苹果也在打造一款可活动的桌面机器人。据介绍,该产品类似于一台安装了可移动机械臂的 iPad,能够多角度旋转以及跟随房间内的用户。

Gurman 指出「其像人的头部一样」,能够实时定位到唤醒人所在方向。而从遐想图显示,苹果的可活动桌面机器人形态类似 DeskMate。

除 DeskMate 亮相外,可以科技同步宣布,计划于 2026 年 2 月底启动新一轮融资路演。所募资金将主要用于下一代产品的研发和全球市场拓展,以期在快速成长的 AI 桌面伙伴赛道中保持领先优势。

LumiMind 发布全球首款实时调控脑电睡眠仪

日前,在 2026 年国际消费电子展(CES 2026)上,专注于脑健康的神经科技创新品牌 LumiMind 正式发布其首款消费级产品——LumiSleep 实时调控脑电睡眠仪。

据悉,该产品基于毫秒级实时脑电监测与个性化神经调控技术,致力于帮助用户自然、舒适地进入睡眠状态,标志着睡眠技术从被动「监测」迈向主动「引导」的新阶段。

相较于主流睡眠产品依赖运动、心率等间接生理数据推断睡眠状态,LumiSleep 则采用截然不同的技术路径——直接依据用户实时脑电信号进行个性化引导。

据介绍,LumiMind 的核心技术源自岩思类脑人工智能研究院的长期基础研究。岩思类脑基于人工智能脑电解码算法框架,结合海量人类颅内脑电数据,已成功研发全球首个脑电大模型,并已应用于多个脑机接口场景。

展会期间,LumiMind 凭借在非侵入式实时脑电调控领域的原创性突破,荣获 CES 2026 全球权威技术奖项(SPEED AWARD),成为本届 CES 脑科学与健康科技领域最受关注的创新企业之一。

一加 Turbo 6 系列正式发布

昨晚,一加正式发布一加 Turbo 6 系列,先看价格:

  • 一加 Turbo 6:首销 2099 元起,国补到手价 1784.15 元起;
  • 一加 Turbo 6V:首销 1699 元起,国补到手价 1444.15 元起。

新机全系采用相同的极简设计。其中 Turbo 6 拥有「追光银」「独行黑」「旷野青」三款配色,配备「金属魔方」Deco 和 15.5mm 超大黄金 R 角,并且 Turbo 6 还支持 IP66/68/69/69K 超豪华满级防尘防水。

性能方面,一加 Turbo 6 搭载同档唯一第四代骁龙 8s 风驰版移动平台。据介绍,该芯片由一加联合高通深度定制,注入独家自研芯片级游戏技术「风驰游戏内核」,并且支持同档唯一的 165Hz 超高帧游戏体验。

而一加 Turbo 6V 搭载同档唯一第四代骁龙 7s 移动平台,配备 144Hz 电竞护眼屏。

一加 Turbo 6 系列均搭载 9000mAh「超巨量」冰川电池,配备 80W 超级闪充,并且配备最高 27W 有线反向快充。

屏幕方面,一加 Turbo 6 搭载同档唯一旗舰级的 1.5K 165Hz 超高刷电竞东方屏。

新消费

阿里:加大投入淘宝闪购以达到市场绝对第一

昨日,多家媒体报道称,阿里巴巴面向投资者的最新交流信息显示,淘宝闪购在最新季度取得关键进展。

据悉,阿里集团对于淘宝闪购 2026 年的投入战略明朗化:闪购首要目标是份额增长,会坚定加大投入以达到市场绝对第一。

据阿里 2026 财年第三季度业绩前瞻信息,淘宝闪购自 2025 年 4 月发力以来,继续保持着强劲发展势头,2025 年 12 月季度 GMV 份额持续增长,订单结构持续改善,比竞对亏损收敛速度更快,非餐即时零售进展快,对远场电商交叉销售符合预期。

阿里高层称,2025 年 12 月季度的进展让公司更有信心,未来几个季度会加大对淘宝闪购的投入,在提升高客单价用户、非餐零售等方面,持续增加用户规模、提升用户粘性。

海底捞「小便门」当事人登报道歉

据澎湃新闻消息,2026 年 1 月 8 日,《人民法院报》3 版刊登了海底捞小便当事人唐某及其父母的道歉声明。

唐某在道歉声明称,「我深刻认识到自己的错误行为,在此向四川新派餐饮管理集团有限公司、上海捞派餐饮管理有限公司表示真诚的歉意。我也接受到了来自家长、学校、公安、法院,以及网络广大消费者的批评与教育,我会吸取深刻的教训,改过自新。」

据此前报道,2025 年 3 月,一则海底捞火锅店内有人「向火锅小便」的短视频引发关注,之后涉案的唐某(男,17 岁)和吴某(男,17 岁)被行政拘留。

2025 年 9 月,上海市黄浦区人民法院判令两名未成年人及其监护人在指定报刊上赔礼道歉,赔偿涉事餐饮公司经济损失共计 220 万元。

挪车二维码被曝成为电诈新入口

据央视新闻报道,日前有不法分子企图通过定制的挪车卡片实施电信诈骗。

报道称,重庆周先生在下单定制挪车卡片后,收到卡片并按照提示扫描二维码下载了一款 App。

然而相应 App 中内置多个聊天群,每个群内都会发布「刷单任务」,宣称用户通过购买商品即可获得返利,「日收益可达 300 至 500 元」。

一开始,周先生并没有轻信所谓的刷单返利,但群里的成员每天都会主动晒出返现收益截图,还附带了提现成功的凭证,这让周先生的心理防线慢慢松动。

他抱着试试看的心态做了几单,没想到确实成功提现了 1000 多元。就这样,周先生开始加大投入,连续几天完成刷单任务后,App 上显示的收益累计高达 5 万多元。

而当周先生要把返利的报酬提取出来时,「客服」告诉他,他输入的银行卡号密码错误,并且需要解锁账户恢复提现功能。而恢复的唯一方法是购置黄金并邮寄到公司,才能完成解绑流程。

周先生赶紧带着 11 万元现金来到重庆北碚区某金店要求购买黄金。然而他的种种异常举动引起了金店店员的警觉。警方接到店员的报案后第一时间赶赴现场。

警方介绍,周先生遭遇的其实是很常见的刷单返利诈骗,但是狡猾的诈骗分子却套了一层定制专属挪车二维码的外壳,相比一般的此类骗局,更具隐蔽性也更具迷惑性。

好看的

迪士尼或入局竖屏视频内容

据 The Verge 报道,迪士尼近期在 CES 2026 上宣布,其将在今年晚些时候在 Disney+ 中推出竖屏视频内容。

迪士尼娱乐副总裁 Erin Teague 接受采访时透露,Disney+ 中的竖屏内容将包括「原创短片节目、社交视频剪辑、从更长形式的剧集或电影中重新制作的场景,或这些内容的组合」。

Teague 称,「随着时间的推移,我们将随着探索各种格式、类别和内容类型的应用,不断发展类似体验,为用户提供实时更新、更感兴趣的内容,涵盖体育、新闻和娱乐,并且根据用户此前访问进行推送。」

《夜王》发布正式预告

据新浪电影消息,贺岁喜剧《夜王》近期发布正式预告,将于 2 月 17 日香港上映。

故事以 2012 年香港尖东夜总会行业没落为背景,在尖东夜场混迹数十年的欢哥面临时代转变的挑战,不得不与前妻 V 姐联手合作,最终两人为守护「东日夜总会」带领团队绝地反击。

影片由《毒舌大状》导演吴炜伦执导,黄子华、郑秀文主演,王丹妮、廖子妤、杨偲泳、谢君豪、杨伟伦、卢镇业、何启华等参演。

《我的朋友安德烈》公布新定档海报

据新浪电影消息,电影《我的朋友安德烈》今日释出「重逢」版定档海报,将于 1 月 17 日公映。

电影改编自双雪涛的同名小说《我的朋友安德烈》,影片讲述性格迥异的李默与安德烈因为足球成为知己,而一场意外却让安德烈跟随年少往事一起消失在了李默的记忆中。多年后,李默在为父亲奔丧路上与安德烈「重逢」,一段尘封的回忆被逐渐揭开。

该片由董子健导演,刘昊然、董子健领衔主演,殷桃特邀出演,韩昊霖、迟兴楷主演,董宝石特别推荐,宁理特别邀请。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


React从入门到出门第四章 组件通讯与全局状态管理

作者 怕浪猫
2026年1月9日 08:55

G9oezq6XEAECJ26.jpeg 大家好~ 前面我们已经掌握了 React 19 的函数组件、Hooks、虚拟 DOM 等核心基础,今天咱们聚焦 React 应用开发中的核心问题——组件间的通信

在 React 应用中,组件不是孤立的,它们需要通过“传递数据”协同工作:小到父子组件间的简单数据传递,大到跨层级、多组件共享的全局状态管理,都是日常开发中高频遇到的场景。

很多新手会在“该用哪种传参方式”“什么时候需要全局状态”这些问题上困惑。今天这篇文章,我们就从“组件关系”出发,按“简单到复杂”的顺序,拆解 React 19 中的组件传参方案,再深入讲解全局状态管理的核心思路与常用方案,结合代码示例和图例,让大家能根据实际场景灵活选择~

一、先明确:组件关系决定传参方案

在 React 应用中,组件间的关系主要分为 3 类:父子组件、兄弟组件、跨层级组件(祖孙/远亲) 。不同关系对应的传参难度和方案不同,我们先通过一个图例理清组件关系模型:

核心原则:能局部传参就不全局——局部传参(如父子、兄弟)简单直观、性能开销小,全局状态(如 Redux、Context)适合共享数据多、跨层级广的场景,避免过度设计。

二、React 19 组件传参方案全解析(按场景分类)

1. 父子组件传参:最基础的“props 向下+回调向上”

父子组件是最常见的关系,传参核心依赖 props:父组件通过 props 向子组件传递数据(向下传),子组件通过 props 接收父组件的回调函数,将数据传递回父组件(向上传),形成“双向通信”。

场景 1:父传子(数据向下传递)

核心逻辑:父组件在使用子组件时,通过“属性=值”的形式传递数据,子组件通过参数 props 接收(可解构简化)。

// 父组件:传递数据给子组件
function Parent() {
  const parentData = "我是父组件的数据";
  const userInfo = { name: "小明", age: 22 };

  return (
    <div>
      <h3>父组件</h3>
      {/* 通过 props 传递基础类型、对象等数据 */}
      <Child 
        msg={parentData} 
        user={userInfo}
        isShow={true}
      />
    </div>
  );
}

// 子组件:接收并使用父组件传递的数据
// 方式 1:直接通过 props 参数接收
// function Child(props) {
//   return <p>父组件传递的消息:{props.msg}</p>;
// }

// 方式 2:解构 props,更简洁(推荐)
function Child({ msg, user, isShow }) {
  return (
    <div>
      <h4>子组件</h4>
      {isShow && <p>父组件传递的消息:{msg}</p>}
      <p>用户姓名:{user.name},年龄:{user.age}</p>
    </div>
  );
}

注意:props 是只读的!子组件不能直接修改 props 的值(如不能写 user.age = 23),若需修改,需通过“子传父”的方式让父组件更新数据。

场景 2:子传父(数据向上传递)

核心逻辑:父组件传递一个“回调函数”给子组件,子组件触发该函数时,将需要传递的数据作为参数传入,父组件在回调函数中接收并处理数据。

// 父组件:传递回调函数给子组件
function Parent() {
  const [childData, setChildData] = useState("");

  // 回调函数:接收子组件传递的数据
  const handleChildMsg = (data) => {
    console.log("子组件传递的数据:", data);
    setChildData(data); // 更新父组件状态
  };

  return (
    <div>
      <h3>父组件</h3>
      <p>子组件传递的消息:{childData}</p>
      {/* 传递回调函数 */}
      <Child onSendMsg={handleChildMsg} />
    </div>
  );
}

// 子组件:触发回调函数,传递数据给父组件
function Child({ onSendMsg }) {
  const [inputValue, setInputValue] = useState("");

  const handleSubmit = () => {
    // 触发父组件传递的回调函数,传入数据
    onSendMsg(inputValue);
    setInputValue(""); // 清空输入框
  };

  return (
    <div>
      <h4>子组件</h4>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入要传递给父组件的内容"
      />
      <button onClick={handleSubmit} style={{ marginLeft: "10px" }}>
        发送给父组件
      </button>
    </div>
  );
}

场景 3:父子双向绑定(表单常见)

核心逻辑:结合“父传子”和“子传父”,父组件传递数据给子组件(表单默认值),子组件通过回调函数将修改后的值传递回父组件,实现“数据同步”。

// 父组件:管理表单状态
function Parent() {
  const [username, setUsername] = useState("");

  // 接收子组件修改后的值,更新父组件状态
  const handleUsernameChange = (newValue) => {
    setUsername(newValue);
  };

  return (
    <div>
      <h3>父组件:{username}</h3>
      {/* 传递状态(默认值)和回调函数 */}
      <Input 
        value={username} 
        onChange={handleUsernameChange} 
        placeholder="请输入用户名"
      />
    </div>
  );
}

// 子组件:表单输入组件
function Input({ value, onChange, placeholder }) {
  // 输入变化时,触发回调函数传递新值
  const handleInput = (e) => {
    onChange(e.target.value);
  };

  return (
    <input
      type="text"
      value={value}
      onChange={handleInput}
      placeholder={placeholder}
      style={{ width: "300px", height: "30px", padding: "0 8px" }}
    />
  );
}

2. 兄弟组件传参:通过父组件中转

兄弟组件间没有直接的通信通道,需通过“共同的父组件”作为中转:先让“发送数据的兄弟”将数据传递给父组件,再由父组件将数据传递给“接收数据的兄弟”。

用图例展示通信流程:

实战案例:兄弟组件数据同步

// 父组件:作为兄弟组件的中转
function Parent() {
  const [sharedData, setSharedData] = useState("");

  // 接收 Child1 传递的数据
  const handleDataFromChild1 = (data) => {
    setSharedData(data);
  };

  return (
    <div>
      <h3>父组件(中转)</h3>
      {/* 兄弟 1:发送数据 */}
      <Child1 onSendData={handleDataFromChild1} />
      {/* 兄弟 2:接收数据 */}
      <Child2 receivedData={sharedData} />
    </div>
  );
}

// 兄弟 1:发送数据的组件
function Child1({ onSendData }) {
  const [inputValue, setInputValue] = useState("");

  const handleSend = () => {
    onSendData(inputValue);
    setInputValue("");
  };

  return (
    <div>
      <h4>兄弟组件 1(发送方)</h4>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入要传递给兄弟的数据"
      />
      <button onClick={handleSend} style={{ marginLeft: "10px" }}>
        发送给兄弟
      </button>
    </div>
  );
}

// 兄弟 2:接收数据的组件
function Child2({ receivedData }) {
  return (
    <div>
      <h4>兄弟组件 2(接收方)</h4>
      <p>收到兄弟 1 的数据:{receivedData || "暂无数据"}</p>
    </div>
  );
}

3. 跨层级组件传参:Context API(React 19 原生方案)

当组件层级很深(如“爷爷→爸爸→儿子→孙子”),或者跨多个层级传递数据时,用 props 层层传递(即“props drilling”)会非常繁琐,且代码可维护性差。这时可以用 React 原生的 Context API 解决。

Context API 的核心作用:创建一个“全局数据容器”,让所有后代组件都能直接访问容器中的数据,无需层层传递 props

使用步骤:3 步搞定 Context 传参

  1. 创建 Context:用 createContext 创建一个 Context 对象(可设置默认值);
  2. 提供 Context:用 Context.Provider 包裹需要共享数据的组件树,通过 value 属性传入共享数据;
  3. 消费 Context:后代组件用 useContext Hook 直接获取共享数据。

实战案例:跨层级共享主题状态

import { createContext, useContext, useState } from 'react';

// 步骤 1:创建 Context(默认值仅在无 Provider 时生效)
const ThemeContext = createContext("light");

// 步骤 2:提供 Context 的组件(通常是顶层组件)
function App() {
  const [theme, setTheme] = useState("light");

  // 共享的方法:切换主题
  const toggleTheme = () => {
    setTheme(prev => prev === "light" ? "dark" : "light");
  };

  // 要共享的数据和方法(封装成对象)
  const contextValue = {
    theme,
    toggleTheme
  };

  return (
    // 步骤 2:用 Provider 包裹组件树,传入共享数据
    <ThemeContext.Provider value={contextValue}>
      <div style={{ padding: "20px" }}>
        <h2>顶层组件(提供 Context)</h2>
        <Parent /> {/* 父组件 */}
      </div>
    </ThemeContext.Provider>
  );
}

// 父组件(中间层级,无需传递 theme 相关 props)
function Parent() {
  return (
    <div style={{ border: "1px solid #ccc", padding: "20px", marginTop: "10px" }}>
      <h3>父组件(中间层级)</h3>
      <Child /> {/* 子组件 */}
    </div>
  );
}

// 子组件(后代组件,直接消费 Context)
function Child() {
  // 步骤 3:用 useContext 获取共享数据
  const { theme, toggleTheme } = useContext(ThemeContext);

  // 根据主题设置样式
  const containerStyle = {
    border: "1px solid #ccc",
    padding: "20px",
    marginTop: "10px",
    background: theme === "light" ? "#fff" : "#333",
    color: theme === "light" ? "#333" : "#fff"
  };

  return (
    <div style={containerStyle}>
      <h4>子组件(消费 Context)</h4>
      <p>当前主题:{theme}</p>
      <button onClick={toggleTheme}>切换主题</button>
    </div>
  );
}

Context API 适合场景:共享“变化不频繁”的全局数据(如主题、用户登录状态、语言设置)。如果需要频繁更新数据,且涉及复杂逻辑(如多组件修改同一状态),建议结合 useReducer 或专门的全局状态管理库。

三、全局状态管理:从 Context+useReducer 到专业库

当应用规模扩大,需要共享的数据增多、状态更新逻辑复杂(如购物车、用户中心、多页面共享筛选条件)时,单纯的 Context API 就不够用了(比如多个组件修改 Context 数据时,逻辑分散,难以维护)。这时就需要“全局状态管理”方案。

React 19 中常用的全局状态管理方案有 3 类:Context+useReducer(原生方案)、Redux Toolkit(生态主流)、Zustand/Jotai(轻量方案) 。我们分别讲解它们的核心思路和适用场景。

1. 原生方案:Context+useReducer(适合中小型应用)

useReducer 是 React 内置的 Hooks,用于处理“复杂状态逻辑”——当状态更新依赖于前一个状态、或者有多个子值需要同步更新时,useReducer 比 useState 更清晰。

Context+useReducer 的核心思路:用 useReducer 管理全局状态的更新逻辑,用 Context 提供和共享状态与 dispatch 方法,实现“状态集中管理+全局共享”。

实战案例:全局购物车状态管理

import { createContext, useContext, useReducer } from 'react';

// 步骤 1:创建 Context
const CartContext = createContext();

// 步骤 2:定义 reducer 函数(集中处理状态更新逻辑)
// reducer 接收两个参数:当前状态 state、动作 action(包含 type 和 payload)
function cartReducer(state, action) {
  switch (action.type) {
    // 新增商品
    case "ADD_ITEM":
      // 先判断商品是否已存在
      const existingItem = state.find(item => item.id === action.payload.id);
      if (existingItem) {
        // 已存在:更新数量
        return state.map(item => 
          item.id === action.payload.id 
            ? { ...item, count: item.count + 1 } 
            : item
        );
      } else {
        // 不存在:新增商品
        return [...state, { ...action.payload, count: 1 }];
      }
    // 删除商品
    case "REMOVE_ITEM":
      return state.filter(item => item.id !== action.payload.id);
    // 清空购物车
    case "CLEAR_CART":
      return [];
    default:
      return state;
  }
}

// 步骤 3:创建 Provider 组件,提供状态和 dispatch
function CartProvider({ children }) {
  // 用 useReducer 管理状态:初始状态为空数组
  const [cartState, dispatch] = useReducer(cartReducer, []);

  // 共享的数据和方法
  const contextValue = {
    cartState, // 购物车状态
    // 封装 dispatch 方法(让组件更易用,无需直接写 action)
    addItem: (item) => dispatch({ type: "ADD_ITEM", payload: item }),
    removeItem: (id) => dispatch({ type: "REMOVE_ITEM", payload: { id } }),
    clearCart: () => dispatch({ type: "CLEAR_CART" })
  };

  return (
    <CartContext.Provider value={contextValue}>
      {children}
    </CartContext.Provider>
  );
}

// 步骤 4:消费全局状态的组件
// 组件 1:商品列表(添加商品到购物车)
function ProductList() {
  const { addItem } = useContext(CartContext);

  // 模拟商品数据
  const products = [
    { id: 1, name: "React 实战教程", price: 99 },
    { id: 2, name: "Vue 实战教程", price: 89 },
    { id: 3, name: "TypeScript 教程", price: 79 }
  ];

  return (
    <div>
      <h3>商品列表</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => addItem(product)}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

// 组件 2:购物车(展示/删除/清空商品)
function Cart() {
  const { cartState, removeItem, clearCart } = useContext(CartContext);

  // 计算总价格
  const totalPrice = cartState.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartState.length} 种商品)</h3>
      {cartState.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartState.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => removeItem(item.id)}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={clearCart} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

// 根组件
function App() {
  return (
    <CartProvider>
      <div style={{ padding: "20px" }}>
        <h2>全局购物车管理(Context+useReducer)</h2>
        <ProductList />
        <Cart />
      </div>
    </CartProvider>
  );
}

2. 生态主流:Redux Toolkit(适合大型复杂应用)

Redux 是 React 生态中最成熟的全局状态管理库,而 Redux Toolkit(RTK)是官方推荐的 Redux 简化方案(解决了原生 Redux 代码繁琐、配置复杂的问题)。

核心优势:状态集中管理、可预测性强、支持中间件(如异步请求)、调试工具完善,适合大型应用中多团队协作、复杂状态逻辑的场景。

核心概念与使用步骤(简化)

  1. 安装依赖:npm install @reduxjs/toolkit react-redux
  2. 创建切片(Slice):用 createSlice 定义状态初始值、reducer 函数(同步/异步);
  3. 创建 Store:用 configureStore 整合所有切片;
  4. 提供 Store:用 Provider(来自 react-redux)包裹根组件;
  5. 消费 Store:用 useSelector 获取状态,用 useDispatch 触发状态更新。

实战案例:Redux Toolkit 实现购物车

// 1. 安装依赖后,创建切片(src/features/cart/cartSlice.js)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 模拟异步请求:从接口获取商品数据(异步 action)
export const fetchProducts = createAsyncThunk(
  'cart/fetchProducts',
  async () => {
    const res = await fetch('https://api.example.com/products');
    return res.json();
  }
);

// 创建切片
const cartSlice = createSlice({
  name: 'cart', // 切片名称(唯一)
  initialState: {
    products: [], // 商品列表
    cartItems: [], // 购物车商品
    loading: false, // 加载状态
    error: null // 错误信息
  },
  reducers: {
    // 同步 action:添加商品到购物车
    addToCart: (state, action) => {
      const existingItem = state.cartItems.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.count += 1;
      } else {
        state.cartItems.push({ ...action.payload, count: 1 });
      }
    },
    // 同步 action:从购物车删除商品
    removeFromCart: (state, action) => {
      state.cartItems = state.cartItems.filter(item => item.id !== action.payload);
    },
    // 同步 action:清空购物车
    clearCart: (state) => {
      state.cartItems = [];
    }
  },
  // 处理异步 action 的状态(pending/fulfilled/rejected)
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        state.products = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});

// 导出同步 action
export const { addToCart, removeFromCart, clearCart } = cartSlice.actions;

// 导出 reducer
export default cartSlice.reducer;

// 2. 创建 Store(src/app/store.js)
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from '../features/cart/cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer // 整合 cart 切片
  }
});

// 3. 根组件提供 Store(src/App.js)
import { Provider } from 'react-redux';
import { store } from './app/store';
import ProductList from './features/cart/ProductList';
import Cart from './features/cart/Cart';

function App() {
  return (
    <Provider store={store}> {/* 提供 Store */}
      <div style={{ padding: "20px" }}>
        <h2>Redux Toolkit 购物车</h2>
        <ProductList />
        <Cart />
      </div>
    </Provider>
  );
}

// 4. 消费 Store:商品列表组件(src/features/cart/ProductList.js)
import { useDispatch, useSelector } from 'react-redux';
import { fetchProducts, addToCart } from './cartSlice';
import { useEffect } from 'react';

function ProductList() {
  const dispatch = useDispatch();
  const { products, loading, error } = useSelector(state => state.cart);

  // 组件挂载时获取商品数据
  useEffect(() => {
    dispatch(fetchProducts());
  }, [dispatch]);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

  return (
    <div>
      <h3>商品列表</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => dispatch(addToCart(product))}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default ProductList;

// 5. 消费 Store:购物车组件(src/features/cart/Cart.js)
import { useDispatch, useSelector } from 'react-redux';
import { removeFromCart, clearCart } from './cartSlice';

function Cart() {
  const dispatch = useDispatch();
  const { cartItems } = useSelector(state => state.cart);

  const totalPrice = cartItems.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartItems.length} 种商品)</h3>
      {cartItems.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => dispatch(removeFromCart(item.id))}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={() => dispatch(clearCart())} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

export default Cart;

3. 轻量方案:Zustand(适合中小型应用,简洁高效)

如果觉得 Redux Toolkit 配置还是繁琐,而 Context+useReducer 在复杂场景下不够灵活,可以选择 Zustand——一个轻量级的全局状态管理库,API 简洁,无需过多配置,深受 React 开发者喜爱。

核心优势:代码简洁、学习成本低、无需 Provider 包裹、支持中间件(异步请求、持久化等) ,适合中小型应用或对开发效率有要求的场景。

实战案例:Zustand 实现购物车

// 1. 安装依赖:npm install zustand
import { create } from 'zustand';
import { useEffect } from 'react';

// 2. 创建 Store
const useCartStore = create((set) => ({
  // 状态
  products: [],
  cartItems: [],
  loading: false,
  error: null,

  // 异步 action:获取商品数据
  fetchProducts: async () => {
    set({ loading: true, error: null });
    try {
      const res = await fetch('https://api.example.com/products');
      const data = await res.json();
      set({ products: data, loading: false });
    } catch (err) {
      set({ error: err.message, loading: false });
    }
  },

  // 同步 action:添加商品到购物车
  addToCart: (product) => {
    set((state) => {
      const existingItem = state.cartItems.find(item => item.id === product.id);
      if (existingItem) {
        return {
          cartItems: state.cartItems.map(item => 
            item.id === product.id ? { ...item, count: item.count + 1 } : item
          )
        };
      } else {
        return { cartItems: [...state.cartItems, { ...product, count: 1 }] };
      }
    });
  },

  // 同步 action:删除商品
  removeFromCart: (id) => {
    set((state) => ({
      cartItems: state.cartItems.filter(item => item.id !== id)
    }));
  },

  // 同步 action:清空购物车
  clearCart: () => {
    set({ cartItems: [] });
  }
}));

// 3. 商品列表组件
function ProductList() {
  // 从 Store 获取状态和方法
  const { products, loading, error, fetchProducts, addToCart } = useCartStore();

  useEffect(() => {
    fetchProducts();
  }, [fetchProducts]);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

  return (
    <div>
      <h3>商品列表(Zustand)</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => addToCart(product)}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

// 4. 购物车组件
function Cart() {
  // 从 Store 获取状态和方法
  const { cartItems, removeFromCart, clearCart } = useCartStore();

  const totalPrice = cartItems.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartItems.length} 种商品)</h3>
      {cartItems.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => removeFromCart(item.id)}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={clearCart} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

// 5. 根组件
function App() {
  return (
    <div style={{ padding: "20px" }}>
      <h2>Zustand 购物车</h2>
      <ProductList />
      <Cart />
    </div>
  );
}

四、全局状态管理方案对比与选择建议

为了让大家能根据项目规模和需求选择合适的方案,我们用表格对比常用的 3 种全局状态管理方案:

方案 核心优势 劣势 适用场景
Context+useReducer 1. 原生方案,无需额外安装依赖;2. 实现简单,学习成本低;3. 轻量无冗余 1. 不支持中间件,处理异步逻辑繁琐;2. 状态更新会触发所有消费组件重渲染(需配合 memo 优化);3. 不适合复杂状态逻辑 中小型应用、简单全局状态(如主题、登录状态)
Redux Toolkit 1. 状态集中管理,可预测性强;2. 支持中间件(异步、日志等);3. 调试工具完善;4. 适合多团队协作 1. 配置相对繁琐,学习成本高;2. 代码量较多;3. 轻量应用可能显得过重 大型复杂应用、多团队协作、复杂状态逻辑(如电商、后台管理系统)
Zustand 1. API 简洁,学习成本低;2. 无需 Provider 包裹;3. 支持中间件,处理异步简单;4. 性能优秀(精准更新) 1. 生态不如 Redux 完善;2. 大型复杂应用的协作规范不如 Redux 成熟 中小型应用、对开发效率有要求的场景、需要轻量方案替代 Context+useReducer

五、核心总结与避坑指南

核心总结

  1. 组件传参遵循“就近原则” :父子/兄弟组件用 props+回调,跨层级用 Context API,全局共享用专门的状态管理方案;
  2. 全局状态管理“按需选择” :小型应用用 Context+useReducer,中型用 Zustand,大型复杂应用用 Redux Toolkit;
  3. props 是只读的:子组件不能直接修改 props,需通过回调让父组件更新,避免破坏单向数据流;
  4. 避免过度设计:不要一开始就用全局状态,先尝试局部传参,当局部传参无法满足需求时再引入全局状态。

避坑指南

  • 坑 1:滥用 Context API:Context 会导致所有消费组件在状态更新时重渲染,若状态更新频繁,需配合 memo、useMemo 优化;
  • 坑 2:Redux 过度使用:不是所有状态都需要放入 Redux,局部状态(如组件内部的表单输入)用 useState 即可;
  • 坑 3:列表渲染忘记加 key:传参时若涉及列表渲染,务必给列表项加唯一 key,避免 React 误判节点导致性能问题;
  • 坑 4:直接修改状态:无论是局部状态还是全局状态,都要遵循“不可变更新”原则(如用扩展运算符、map 等方法创建新状态),避免直接修改原状态。

六、下一步学习方向

今天我们掌握了 React 19 组件传参与全局状态管理的核心方案,下一步可以重点学习:

  • 状态管理性能优化:如 memo、useMemo、useCallback 与状态管理的配合使用;
  • 其他轻量状态管理库:如 Jotai、Recoil(原子化状态管理,适合细粒度状态共享);
  • Redux 高级特性:如中间件(redux-thunk、redux-saga)、状态持久化(redux-persist);
  • React 19 新增状态相关特性:如 useOptimistic(乐观更新)、useActionState(表单状态管理)。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题也可以在评论区留言交流~ 我们下期再见!

科技爱好者周刊(第 380 期):为什么人们拥抱"不对称收益"

作者 阮一峰
2026年1月9日 08:11

这里记录每周值得分享的科技内容,周五发布。

本杂志开源,欢迎投稿。另有《谁在招人》服务,发布程序员招聘信息。合作请邮件联系(yifeng.ruan@gmail.com)。

封面图

中法合作的一个艺术项目《挑战第841次》,让路过的行人在黄浦江边的一个玻璃亭子里,弹奏法国作曲家的一个钢琴作品。(via

为什么人们拥抱"不对称收益"

前两周,我跟大家说,美国现在最流行"预测市场"。我当时没有统计数字,现在有了。

2025年11月,美国前两大预测市场---- PolymarketKalshi ---- 一共成交了超过100亿美元。

看这个数字,大家可能没感觉。作为对比,美国全国的体育彩票,2024年的销售额是137亿美元。

这就是说,预测市场一个月的交易量,接近了体育彩票全年的销售额。要知道,这两个网站6年前都还不存在!

这么恐怖的增长速度,难怪美国各大公司现在都想挤入这个市场,分一杯羹。

预测市场就是变相的网络彩票,它的火爆只能说明一件事情,美国正出现疯狂的"彩票热"。

本周,我看到一篇文章(上图),一位风险投资家分析这个现象。我想分享他的观点,他认为,预测市场火爆的根本原因,是社会心态的焦虑和绝望

(1)财富转移机制失效了,通过正常工作致富,越来越不可能。工资的增长速度,低于消费的增长速度,个人债务正在变多。

虽然资产的价格(比如股票、黄金、房产)也在上涨,但只是让那些拥有资产的人受益,对于没有这些资产的穷人,只是变得更贫穷。

(2)传统的人生模式也失效了。以前的模式是,找一家大公司,每天按时上班,努力工作,对公司忠心耿耿,坚持多年就会得到回报。你会收到公司的奖励,退休后还有养老金。

这种模式现在行不通了。公司的经营短期化,能存活20年的公司并不多,更不要说你的岗位了。一旦失去现在的工作,再次就业非常困难,以前的工作经验很可能用处不大。

(3)AI 的出现,加剧了前两种情况的发展速度。AI 让一切加速了,压缩了时间。以前,你有五年的时间奋斗,AI 让你感到必须在一年里拿到结果,否则就可能为时已晚。

(4)社交媒体则使得人们永远不会对现状满意。

以前,你的参照群体只是周围人群,现在的参照群体是全世界。你每天看到的都是收入高、赚钱容易、生活优渥的人群,永远会让你感到自己的生活不够好,而无论你已经取得了怎样的成就。

(5)结果就是,越来越多的人失去了耐心,不再相信长期投入,不再幻想长期的劳动积累会通往圆满的人生,社会也不奖励耐心。

为什么要苦苦奋斗20年,去争取10年后可能根本不存在的晋升机会?我要的是一条快速的道路,摆脱日常生活的困境,而且越快越好。

(6)这种心态下,人们的风险偏好发生了变化。为了快速摆脱困境,在风险更大的选项上放手一搏,成了合理的选择

即使只有5%的希望,也比100%的停滞不前更有吸引力。这就是彩票在贫困社区更畅销的原因。

这在经济学上称为"不对称收益"(asymmetric returns),就是风险和收益不对称。失败的可能性很大,但只会损失一小笔钱,成功的可能性很小,但是一旦成功,就会获得巨大收益,简单说就是"小亏大赚"。

追求不对称收益,已经成了一种普遍的心态。它推动了前几年的加密货币和 NFT 的热潮,现在又推动了预测市场。

可以确定,凡是能够产生"不对称收益"的事情,今后都会迅速成为热点。

新人上手 Claude Code 的简单方案

AI 编程工具,我用的是 Claude Code。以前推荐过,非常好用,功能很强。

我现在依然这样认为,但是必须说,Claude Code 不适合所有人,有使用门槛

它要求用户熟悉命令行,而且 Windows 安装不方便,需要启用 Linux 子系统 WSL。另外,如果在外面,没有自己的计算机,临时想用一下,也很麻烦。

元旦的时候,我在广东听说,有人做了"云端 Claude Code 客户端",解决了这些痛点,就很感兴趣。

他们团队叫做 302.AI,我以前就有接触。他们做云端服务很多年了,现在专注于 AI 模型接入。大家可以去官网看一下,用他们的 API 能够接入几乎所有主流模型,数量有几百个。

他们跟我一样,也感到 Claude Code 的诸多不便,就想能不能再开发一个它的客户端,封装所有复杂性,提供最好用的 AI 编程体验。

(1)跨平台桌面应用。他们提供 Win/Mac/Linux 安装程序,通过桌面窗口去使用云端的 Claude Code。

(2)零配置的云端沙盒。云端的 Claude Code 预装在一个沙盒里,集成了 Node.js、Python、Git、CMake、build-essential 等开发工具,不需要任何本地环境配置,开箱即用。

同时,沙盒也保障了安全,跟本地电脑是隔离的,AI 就不会误删本地文件。

(3)对话界面。对于不习惯命令行的用户,他们提供对话式交互界面(Chat UI),以聊天方式完成编程。

(4)随意更换模型。Claude Code 更换底层模型,需要配置环境变量,他们的客户端不需要这么麻烦,只需要鼠标选中即可。

你可以直接用他们的 API,也可以配置自己的 API Key。

(5)一键部署。他们还提供了部署功能,AI 生成的结果可以一键发布到公网,直接访问,无需购买服务器或配置域名。

可以说,这个方案完全针对 Claude Code 的各种痛点,目标是打造新手最容易上手的 Vibe Coding 工具。

感兴趣的朋友可以去 studio.302.ai 下载,体验一下。(提醒:使用前需要注册/登录 302.AI 账号。)

科技动态

1、乔布斯写的程序

乔布斯创立苹果公司之前,当过短时间的程序员。1975年,他20岁,从大学退学后,进入雅达利公司写电子游戏。

人们一直不知道,他的编程水平如何,现在终于曝光了。

本周,乔布斯的一些个人档案公开拍卖,其中就有当年他写的程序,打印纸上还有他的亲笔注释。

有人把这个程序还原出来,放到虚拟机上跑,终于让我们看到了乔布斯的软件作品。

这个程序叫做 AstroChart,跟星座有关。用户提供出生的时间地点,它会显示太阳系主要天体的位置。

从代码来看,乔布斯的编程水平可以,他使用三角函数计算行星位置,并且绕过当年硬件没有双精度浮点数的限制,用整数除法代替。

2、世界最大电动船

澳大利亚建造了世界最大的电力轮船,长度130米,里面的电池重达250吨。

这艘船将用作阿根廷与乌拉圭之间的轮渡,可以搭载多达2100名乘客和225辆汽车。

这艘船不仅是史上最大的电动船,可能也是史上最大的电动装置,一次可以携带超过4万度电。

3、最高过山车

2025年的最后一天,沙特阿拉伯在距离首都利雅得40分钟车程的地方,开张了一个乐园。

这个乐园有27个游乐设施,很多都是世界之最,其中就有目前世界最高的过山车。

这个过山车高达195米,相当于60层楼,比先前的世界纪录高出了55米。

整个过山车的长度是4.2公里,最高速度可以达到240公里/小时,全程只有3分多钟。

网上有很多这个过山车的视频,不要说坐在车上,就是看视频都觉得惊心动魄。

文章

1、2025年大模型回顾(英文)

西蒙·威利森(Simon Willison)的 AI 年度回顾,过去一年的大事件基本都提及了,总结和评点得非常好,推荐阅读。

2、华为的 5nm 制程怎么样?(英文)

这是一家美国技术媒体对华为麒麟9030芯片(搭载于最新的 Mate 80 手机)的分析文章。

该文认为,该芯片比早先的 7nm 制程有提升,是大陆制造的最先进芯片,但从跑分看,还没达到台积电的 5nm 水平。文章有中文版

3、Opus 4.5 将会改变一切(英文)

作者不相信 AI 会取代程序员,直到遇到 Anthropic 公司的 Opus 4.5 模型。本文是他的4个项目的编程体会,他现在确信程序员会被替代。

4、HTTP caching, a refresher(英文)

对于 HTTP 缓存机制的一个总体介绍,梳理浏览器缓存的处理逻辑。

5、Vitest 的浏览器模式介绍(英文)

JS 测试框架 Vitest 4.0 引入了浏览器模式,可以进行浏览器自动化,类似于 Playwright,进行 UI 测试,本文是一个简单介绍。

6、如何提高 JS 数组的读写速度(英文)

一篇 JavaScript 中级教程,介绍通过为 JS 数组分配连续内存,提高数组的读写速度。

工具

1、ZenOps

一个命令行工具,在本地终端里查询阿里云/腾讯云等云平台的运行数据,并提供钉钉、飞书、企微机器人,进行自然语言查询。(@eryajf 投稿)

2、白虎面板

轻量级的服务器定时任务管理系统,适合低配置的服务器。(@engigu 投稿)

3、OnlinePlayer

一个网页播放器,可以播放本地视频和云盘视频。(@13068240601 投稿)

4、gitstats

命令行工具,生成 Git 仓库的统计数据。(@shenxianpeng 投稿)

5、云图

一个极简风格的图床,可以搭建到自己的 NAS,提供灵活的 API。(@qazzxxx 投稿)

6、KeyStats

开源的 macOS 小工具,对按键行为进行统计。(@debugtheworldbot 投稿)

7、py2dist

这个工具可以将 Python 脚本编译成二进制模块,方便隐藏源码。(@xxnuo 投稿)

8、Stream Panel

Chrome 浏览器开发者工具的一个扩展,用来调试服务器发送事件 (SSE) 和 Fetch 的流式连接。(@bywwcnll 投稿)

9、Zedis

Redis 的图形客户端,跨平台的桌面应用,不使用 Electron,而是使用 Rust + GPUI,性能更好。(@vicanso 投稿)

10、QDav

这个网站可以为夸克网盘加入 WebDAV 协议,从而挂载到网盘播放器来播放夸克网盘的视频。(@ZhouCai-bo 投稿)

11、XApi

开源的 Chrome 浏览器插件,自动捕获当前网页的 Fetch 与 XHR 网络请求,支持改写 Cookie、Origin、Referer 字段,方便开发调试。(@lustan 投稿)

12、PDFCraft

纯浏览器的 PDF 开源工具集,目前有80多个工具。(@pccprint 投稿)

AI 相关

1、Open-AutoGLM

智源公司的开源安卓应用,使用自然语言,让 AI 操作手机,进行手机自动化,可以接入各种模型,无需电脑端。(@Luokavin 投稿)

2、Claude-Ally-Health

一个基于 Claude Code 的个人医疗数据中心,定义了一组自己的命令和技能,用 AI 分析个人医疗数据(体检报告、影像片子、处方单、出院小结)。(@huifer 投稿)

3、灵猫

免费的 AI 图片去水印网站,但只是去除视觉水印,嵌入的数字水印还在。(@pangxiaobin 投稿)

4、DeepDiagram AI

开源的 AI 应用,用自然语言驱动内置的 mermaid、echarts、mindmap、Draw.io 等绘图工具生成图表。(@twwch 投稿)

资源

1、100万首页截图

这个网站收集了100万个热门网站的首页截图,将它们做在一个页面,可以放大查看。

2、Emulator Gamer

各种老游戏机的经典游戏,通过模拟器免费在线游玩。(@SinanWang 投稿)

图片

1、如今的 Mozilla

Mozilla 浏览器的新任 CEO 宣称,公司的发展方向是 AI 浏览器

这让 Mozilla 社区感到担忧,因为没人是为了 AI 而使用它。一位使用者就画了下面这张图。

Mozilla 的吉祥物----一只小狐狸拿着锯子,把自己正坐着的树枝锯断,旁边还有一只鸟,为它递上更锋利的电动锯子,上面写着"AI"。

这张图比喻 Mozilla 一直在自寻死路,全力转向 AI 只会死得更快。

文摘

1、外卖应用的秘密

我是一个大型外卖应用的开发者,受一项严格的保密协议约束。但是,我已经不在乎了,我昨天向公司递交了离职报告。

说实话,我希望公司能起诉我,这样一来,这些事情就会曝光。

我已经消极工作大约八个月了,只是看着代码被推送到生产环境。一想到自己参与了这台机器,我夜里都睡不着。

人们总怀疑算法对用户不利,现实比这更糟。我是一名后端工程师,每周参加产品会议,产品经理(PM)讨论如何才能挤出额外0.4%的利润,他们把用户当成有待开发的资源。

公司有一个"优先配送"服务,你多付2.99美元,就可以更快拿到外卖。这完全是个骗局,根本没有加快派送的速度,而是人为把非优先订单延迟5到10分钟,让你感觉优先订单更快。我们仅仅通过让标准服务变差,就赚取了数百万美元的纯利润,而不是真正改善服务。

最让我恶心的是"绝望分数",这是一个隐藏的外送员指标,根据外送员的行为判断他们多想赚钱。

如果外送员在晚上10点登录系统,毫不犹豫地立即接下每一个3美元的垃圾订单,算法会将他们标记为"高度绝望"。一旦被标记,系统就会停止向他们显示高价订单,理由是"既然我们知道他绝望到愿意接受3美元,为什么还要让他看到15美元的订单呢?"。系统把高价订单留给"休闲"外送员,即那些不愿接低价单的外送员,吸引他们接单,而全职外送员则被碾压成尘埃。

公司还会从用户的账单扣除一笔1.50美元的"外送员福利费",这个名字让用户感觉在帮助外送员。实际上,这笔钱流入了游说反对外送员成立工会的基金,这是公司用于"政策防御"的费用。用户实际上是在为那些高端律师付费,那些律师为削弱外送员的权益而工作。

最后,虽然公司不再从外送员的小费里面提成,因为被起诉过,但是使用其他方法窃取小费。

如果算法预测你是"可能支付小费的用户",而且你很可能会给10美元小费,那么公司只会给外送员可怜的2美元基本派送费。如果你给了0美元小费,公司会给外送员8美元的基本派送费。结果是用户的小费并没有奖励外送员,而是在补贴公司。用户给外送员付工资,这样我们就不用付了。

言论

1、

在美国东海岸(纽约和华盛顿),人们会问:"中国是否就要失败了",而在西海岸(洛杉矶和旧金山),人们更倾向于问:"万一中国成功了会怎样?"

这一定程度上反映了硅谷的特点:更注重收益最大化,而非风险最小化。东海岸的问题也值得认真对待,但过分关注中国是否失败,会助长一种美国无需做出任何改变就能击败对手的论调,从而削弱美国改革的紧迫性。

-- Dan Wang《2025年度信件》

2、

如果美国或中国在某个方面落后太多,落后者就会奋起直追。这将是未来数年甚至数十年世界变化的动力。

-- Dan Wang《2025年度信件》

3、

程序员对待 AI 有两种态度:一种以结果为导向,渴望通过 AI 更快拿到结果;另一种以过程为导向,他们从工程本身获得意义,对于被剥夺这种体验感到不满。

-- Ben Werdmuller

4、

AI 数据中心的建设热潮,导致内存价格暴涨,进而产生一系列连锁反应。

手机和电脑厂商别无选择,只能提价。我们估计,2026年全球的手机市场和电脑市场都会萎缩。手机萎缩2.9%到5.2%,电脑萎缩4.9%到8.9%。

-- IDC 公司的预测

5、

eSIM 手机卡一旦更换就可能失效,相比之下,实体 SIM 卡可以随意插上插下,几乎不会出现故障。推广 eSIM 的后果就是,手机号丢失的事件会大大增多。

-- 《我后悔使用 eSIM》

往年回顾

一切都要支付两次(#333)

没有目的地,向前走(#283)

生活就像一个鱼缸(#233)

腾讯的员工退休福利(#183)

(完)

文档信息

  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证
  • 发表日期: 2026年1月 9日

手写 Vue 模板编译(生成篇)

2026年1月9日 00:47

前言

写本文的背景 《鸽了六年的某大厂面试题:你会手写一个模板引擎吗?》

阅读本文前请务必先阅读解析篇 《手写 Vue 模板编译(解析篇)》

复习

在前文 《手写 Vue 模板编译(解析篇)》 中,我们已经知道,模板编译的过程分为三步:解析、优化、生成。

  1. 解析 parse:在这一步,Vue 会解析模板字符串,并生成对应的 AST
  2. 优化 optimize:这个阶段主要是通过标记静态节点来进行语法树优化。
  3. 生成 generate:利用前面生成 AST(抽象语法树)转换成渲染函数(render function)。

在前文中,我们已经学习了如何生成 AST 接下来我们需要学习如何 optimize 和 generate。

optimize:优化 AST

第一步:标记静态节点

如上文所说,这个阶段主要是通过标记静态节点来进行语法树优化,在进行优化后,Vue 会在 AST 中标记某个节点是否为静态节点。

在上一节生成 AST 中,我们定义了节点类型:

  • 元素节点 type=1
  • 表达式节点 type=2
  • 文本节点 type=3

在不存在子节点时:

  • 首先文本节点必为静态节点,因为文本内容固定不变。
  • 其次表达式则必不为静态节点,因为它的值依赖于引用的表达式。
  • 最后普通节点,如果有 v-ifv-for 指令就是动态节点,否则是静态节点

    注:实际 Vue 中还有 v-bind 等指令也会让节点变为动态,这里简化处理

/**
 * 判断一个节点是否为静态节点
 */
function isStatic(node) {
  // 如果节点是表达式节点,则不是静态节点
  if (node.type === 2) {
    return false
  }

  // 如果节点是文本节点,则是静态节点
  if (node.type === 3) {
    return true
  }

  // 如果节点没有 v-if 和 v-for 指令,则是静态节点
  return !node.if && !node.for
}

对于有子节点的情况:父节点必须满足以下两个条件才能成为静态节点:

  1. 自身是静态节点(没有 v-ifv-for 等动态指令)
  2. 所有子节点都是静态节点

接下来我们处理节点树

/**
 * 标记一个节点是否为静态节点
 */
function markStatic(node) {
  // 先用 isStatic 判断当前节点自身是否为静态
  node.static = isStatic(node)
  // 如果是元素节点,需要检查子节点
  if (node.type === 1) {
    // 遍历所有的子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      // 先递归处理子节点
      markStatic(child)
      // 只要有一个子节点是动态的,父节点也必须是动态的
      if (!child.static) {
        node.static = false
      }
    }
  }
}

第二步:标记静态根节点

接下来是 Vue 优化系统的另一个关键部分 markStaticRoots

markStaticRoots 函数用于标记静态根节点,被标记为静态根节点的元素及其子树会在代码生成阶段被特殊处理:

  1. 提升为常量:将其渲染代码提升到 staticRenderFns 数组中,只生成一次
  2. 跳过 patch:更新时直接复用,不需要重新创建和对比 VNode
  3. 性能提升:减少运行时的计算开销
/**
 * 标记静态根节点
 * @param {Object} node - AST 节点
 */
function markStaticRoots(node) {
  if (node.type === 1) {
    // 只有元素节点才处理

    // 判断是否为静态根节点:必须是静态节点 + 有子节点
    if (node.static && node.children.length) {
      node.staticRoot = true
      // 找到静态根后直接返回,子节点会被整体提升,无需继续遍历
      return
    } else {
      node.staticRoot = false
    }

    // 递归处理所有子节点
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i])
      }
    }

    // 处理 v-if 的其他分支(v-else-if、v-else)
    // 注意:从 i=1 开始,因为 ifConditions[0] 就是当前节点
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block)
      }
    }
  }
}

现在我们来实现 optimize

function optimize(root) {
  if (!root) return
  // 第一步:标记静态节点
  markStatic(root)
  // 第二步:标记静态根
  markStaticRoots(root)
}

举个例子来看看效果:

// 要处理的模板字符串
const str = `<div><h1 v-if="true">hello</h1><h2>cookie</h2></div>`

// 解析为 ast
const ast = parse(str, {
  // ...
})
// 优化 ast
optimize(ast)

console.dir(ast, {
  depth: null,
})

打印结果:

<ref *2> {
  type: 1,
  tag: 'div',
  attrsList: [],
  attrsMap: {},
  children: [
    <ref *1> {
      type: 1,
      tag: 'h1',
      attrsList: [],
      attrsMap: { 'v-if': 'true' },
      children: [ { type: 3, text: 'hello', static: true } ],
      if: 'true',
      ifConditions: [ { exp: 'true', block: [Circular *1] } ],
      parent: [Circular *2],
      static: false,
      staticRoot: false
    },
    {
      type: 1,
      tag: 'h2',
      attrsList: [],
      attrsMap: {},
      children: [ { type: 3, text: 'cookie', static: true } ],
      parent: [Circular *2],
      static: true,
      staticRoot: true
    }
  ],
  static: false,
  staticRoot: false
}

可以观察到 <h2> 节点被标记了静态根节点。

生成代码前置知识

1、new Function

我们知道创建函数除了常用的函数声明、函数表达式、箭头函数以外,还有一个不常用的:构造函数。语法如下:

new Function(corpsFonction)
new Function(arg1, corpsFonction)
new Function(arg1, ...argN, corpsFonction)
// 例:
const addFn = new Function('a', 'b', 'return a + b')
const sum = addFn(1, 2) // 3
  • argN:可选,零个或多个,函数形参的名称,每个名称都必须是字符串
  • corpsFonction:一个包含构成函数定义的 JavaScript 语句的字符串。

2、with 语句

with 可以将一个对象的属性添加到作用域链的顶部,让我们在代码块内直接访问对象的属性。

with (对象) {
  // 在这里可以直接访问对象的属性
}

在 Vue 模板中我们写 {{message}},实际上访问的是 this.message。使用 with(this) 可以省略 this. 前缀,让生成的代码更简洁。

示例:

function foo() {
  let name = 'other' // 局部变量
  let obj = {
    name: 'cookie', // 对象属性
  }
  with (obj) {
    console.log(name) // 优先从 obj 中查找,输出 'cookie'
  }
}
foo() // 输出: cookie

generate 生成代码

终于到了模板编译的最后一步,生成代码!在这一步,我们将根据前面得到的 AST 生成 render 函数。

1、整体入口:generate

generate 是代码生成的入口函数,负责将 AST 转换为可执行的渲染代码:

  1. 递归生成代码:调用 genElement 遍历 AST,生成类似 _c('div', [...]) 的代码字符串
  2. 包装 with 作用域:用 with(this){} 包裹,让代码能访问 Vue 实例的属性
  3. 收集静态渲染函数:将静态根节点单独提取到 staticRenderFns 数组中
/**
 * 代码生成器入口
 * @param {Object} ast - 经过 parse 和 optimize 处理后的抽象语法树
 * @returns {Object} 返回包含 render 函数和 staticRenderFns 数组的对象
 */
function generate(ast) {
  // state 用于存储编译过程中的状态
  // staticRenderFns: 用于收集静态根节点的渲染函数,这些函数只需要生成一次,后续渲染可以直接复用,提升性能
  const state = { staticRenderFns: [] }

  // 递归生成核心渲染代码字符串
  // 例如:_c('div',[_v(_s(message))])
  const code = genElement(ast, state)

  return {
    // render: 主渲染函数的代码字符串
    // 使用 with(this) 包装后,模板中的变量(如 {{message}})能直接从 Vue 实例上获取
    // with(this) 会将 Vue 实例添加到作用域链顶部
    // 这样 message 会自动从 this.message 获取,而不需要在模板里写 this.message
    render: `with(this){return ${code}}`,
    // staticRenderFns: 静态根节点渲染函数的数组
    staticRenderFns: state.staticRenderFns,
  }
}

2、核心函数:genElement

genElement 是代码生成的调度中心,它根据节点类型来使用不同的处理函数。

/**
 * 生成元素的渲染代码
 * @param {Object} el - AST 元素节点
 * @param {Object} state - 渲染状态,包含 staticRenderFns 数组
 * @returns {string} 渲染代码字符串,如:_c('div', [...])
 */
function genElement(el, state) {
  // 1:处理静态根节点
  // el.staticRoot: 在 optimize 阶段标记的静态根节点
  // el.staticProcessed: 防止重复处理的标记
  if (el.staticRoot && !el.staticProcessed) {
    // 静态根节点会被提升为单独的函数存储在 staticRenderFns 中
    // 返回类似 _m(0) 的代码,0 是在 staticRenderFns 数组中的索引
    return genStatic(el, state)
  }
  // 2:处理 v-for 指令
  // el.for: 在 parse 阶段解析的 v-for 属性
  // el.forProcessed: 防止递归时重复处理
  else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  }
  // 3:处理 v-if 指令
  // el.if: 在 parse 阶段解析的 v-if 条件表达式
  // el.ifProcessed: 防止递归时重复处理
  else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  }
  // 4:处理普通元素
  else {
    // 生成标签名字符串,如 'div'
    const tag = `'${el.tag}'`

    // 递归生成所有子节点的代码
    // 返回类似 [_v("hello"), _c('span')] 的数组代码
    const children = genChildren(el, state)

    // 最终调用 _c (createElement) 创建 VNode
    // 生成代码示例:_c('div', [_v("hello")])
    // 如果没有子节点,生成:_c('div')
    return `_c(${tag}${children ? `,${children}` : ''})`
  }
}

处理子节点:genChildren 和 genNode

genElement 中调用了 genChildren 来遍历子节点,genChildren 内部又使用 genNode 来处理每一个子节点,

/**
 * 生成子节点数组的渲染代码
 * @param {Object} el - 父元素节点
 * @param {Object} state - 渲染状态
 * @returns {string} 子节点数组代码,如:[_v("text"), _c('span')]
 */
function genChildren(el, state) {
  const children = el.children
  if (children.length) {
    // 获取子节点数组需要的规范化类型
    const normalizationType = getNormalizationType(children)
    // 遍历所有子节点,为每个子节点生成代码
    return `[${children.map((c) => genNode(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

/**
 * 根据节点类型调用对应的生成函数
 * @param {Object} node - AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} 节点渲染代码
 */
function genNode(node, state) {
  if (node.type === 1) {
    // type === 1: 元素节点,递归调用 genElement
    return genElement(node, state)
  } else {
    // type === 2/3: 文本节点或表达式节点,调用 genText
    return genText(node)
  }
}

为什么需要 getNormalizationType

因为 v-for 会生成数组,导致 children 出现嵌套数组的情况:

<div>
  <span>first</span>
  <span v-for="item in [1,2,3]">{{item}}</span>
  <span>last</span>
</div>

生成的子节点结构会是:

[span_first, [span_1, span_1, span_1], span_last]

Vue 的 createElement 需要知道如何处理这种嵌套,所以要告诉它规范化类型:

  • 不需要规范化(没有嵌套)- type = 0
  • 简单规范化(一层嵌套,如组件)- type = 1
  • 完全规范化(多层嵌套,如 v-for)- type = 2
/**
 * 确定子节点数组需要的规范化(normalization)类型
 * @param {Array} children - 子节点数组
 * @returns {number} 0 | 1 | 2 - 规范化类型
 */
function getNormalizationType(children) {
  let res = 0 // 默认不需要规范化

  // 遍历所有子节点,检测是否需要规范化
  for (let i = 0; i < children.length; i++) {
    const el = children[i]

    // 跳过非元素节点(type=2 表达式节点、type=3 文本节点)
    // 只有元素节点(type=1)才可能需要规范化处理
    if (el.type !== 1) continue

    // 检查是否需要完全规范化(优先级最高,返回 2)
    if (
      el.for !== undefined || // 当前节点有 v-for
      (el.ifConditions && // 或者 v-if 条件分支中有v-for
        el.ifConditions.some((c) => c.block.for !== undefined))
    ) {
      // 需要完全规范化:因为 v-for 会返回数组,需要递归扁平化
      // 例如:[_v(" "), _l(arr, ...), _v(" ")] 其中 _l 返回 [span1, span2, span3]
      // 实际结构是:[_v(" "), [span1, span2, span3], _v(" ")] 需要扁平化为一维数组
      res = 2
      break // 找到一个需要完全规范化的就可以退出,不需要继续检查
    }

    // 检查是否需要简单规范化 返回 1
    // 当前节点可能是组件时,组件可能返回多个根节点(数组)
    // 但组件的 render 函数已经返回规范化的 VNode,所以只需要一层扁平化
    // 我们的模板里不处理组件,这里也就不用考虑这种情况
    // res = 1
  }

  return res // 返回 0、1 或 2
}

3、分类处理函数

Vue 内部函数

首先在 Vue 内部,有一些简写函数,在后续生成代码的时候会用到,列表如下:

缩写 全称 作用 示例
_c createElement 创建元素 VNode _c('div', [...])
_v createTextVNode 创建文本 VNode _v("hello")
_s toString 将变量转为字符串 _v(_s(message))
_l renderList 渲染列表(v-for) _l(items, fn)
_m renderStatic 渲染静态内容 _m(0)
_e createEmptyVNode 创建空节点(v-if 失败时) _e()

静态节点 (genStatic)

静态根节点会被提升为单独的函数,提升性能:

/**
 * 生成静态根节点的渲染代码
 * @param {Object} el - 静态根节点
 * @param {Object} state - 渲染状态
 * @returns {string} 静态节点引用代码,如:_m(0)
 */
function genStatic(el, state) {
  // 标记已处理,防止重复处理
  el.staticProcessed = true

  // 递归生成静态节点的完整代码
  const code = genElement(el, state)

  // 将静态节点函数存储到 staticRenderFns 数组中
  state.staticRenderFns.push(`with(this){return ${code}}`)

  // 返回对静态函数的引用,_m(index) 表示调用 staticRenderFns[index]
  // 索引是当前数组长度减 1
  return `_m(${state.staticRenderFns.length - 1})`
}

静态渲染函数和主 render 函数(vm._render)独立,需要单独设置 with 作用域。

文本处理 (genText)

/**
 * 生成文本节点的渲染代码
 * @param {Object} text - 文本 AST 节点
 * @returns {string} _v('...') 或 _v(_s(variable))
 */
function genText(text) {
  // type 2 是带 {{}} 的表达式,type 3 是普通纯文本
  // 表达式直接使用解析好的 expression 纯文本需要用 JSON.stringify 转义
  // 比如 text 为 "hello":生成代码 `v(hello)` 此时 JS 会去找名为 hello 的变量,这会导致报错(ReferenceError)
  // JSON.stringify("hello") 会返回 "\"hello\""(即带双引号的字符串)。生成的代码会变成:_v("hello")
  const value = text.type === 2 ? text.expression : JSON.stringify(text.text)
  return `_v(${value})`
}

条件渲染 (v-if)

在 Vue 中我们可以使用 v-if/else-if/else 指令,比如下面的模板:

<div v-if="a">A</div>
<div v-else-if="b">B</div>
<div v-else>C</div>

我们生成的 AST 为:

conditions = [
  { exp: 'a', block: { AST节点A } }, // v-if
  { exp: 'b', block: { AST节点B } }, // v-else-if
  { exp: undefined, block: { AST节点C } }, // v-else (没有exp)
]

v-if/else-if/else 的本质就是多重条件判断,在 JavaScript 中最适合用嵌套三元表达式来表达:

// 目标:生成这样的代码
// (a) ? A节点 : (b) ? B节点 : C节点
a ? _c('div', 'A') : b ? _c('div', 'B') : _c('div', 'C')

接下来是代码实现,我们处理 conditions 时,每次弹出第一个条件块,如果为真,则展示当前模块,如果为假,则递归处理剩余的 conditions,如果没有条件(v-else),则直接展示该模块。

/**
 * 生成 v-if 指令的入口函数
 * @param {Object} el - 带有 v-if 的 AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} 三元表达式形式的渲染代码
 */
function genIf(el, state) {
  // 标记已处理,防止递归时重复处理
  el.ifProcessed = true
  // 使用 slice() 复制数组,避免 shift() 修改原始的 ifConditions 数组
  return genIfConditions(el.ifConditions.slice(), state)
}

/**
 * 生成 v-if/else-if/else 指令的渲染代码
 * @param {Array} conditions - 条件数组
 * @param {Object} state - 渲染状态
 * @returns {string} 三元表达式形式的代码
 */
function genIfConditions(conditions, state) {
  // 递归终止条件:所有分支都处理完了 返回空节点
  if (!conditions.length) return '_e()'

  // 取出第一个条件
  const condition = conditions.shift()
  if (condition.exp) {
    // 有条件表达式:v-if 或 v-else-if
    // 生成:(条件) ? 满足时的内容 : 递归处理剩余条件
    return `(${condition.exp})?${genElement(
      condition.block,
      state
    )}:${genIfConditions(conditions, state)}`
  } else {
    // 没有条件表达式:v-else
    // 兜底分支,直接生成节点
    return genElement(condition.block, state)
  }
}

列表渲染 (v-for)

最后我们处理 v-for 指令,考虑下面的例子:

<div v-for="(item, index) in items">{{ item.name }}</div>

在 parse 阶段,这个指令会被解析为 AST 节点的属性:

{
  for: "items",      // 要遍历的数据源
  alias: "item",     // 当前项的别名
  iterator1: "index" // 索引的别名(可选)
}

我们通过_l 来进行遍历。_l 是 Vue 的内部函数 renderList,它的作用是遍历数组/对象,为每一项调用渲染函数。我们要生成的代码格式是:

_l(数据源, function (item, index) {
  return 每一项的渲染代码
})

代码实现:

/**
 * 生成 v-for 指令的渲染代码
 * @param {Object} el - 带有 v-for 的 AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} _l() 函数调用代码
 */
function genFor(el, state) {
  // 1. 提取遍历的相关信息
  const exp = el.for // 遍历的对象
  const alias = el.alias // 别名 item

  // 2. 处理索引参数(可选)
  // 如果有索引,格式为 ",index";没有则为空字符串
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  // 3. 标记已处理,防止递归时重复处理
  el.forProcessed = true

  // 4. 生成 _l() 函数调用
  return `_l((${exp}),function(${alias}${iterator1}${iterator2}){return ${genElement(
    el,
    state
  )}})`
}

最后会转化为代码:

_l(items, function (item, index) {
  return _c('div', [_v(_s(item.name))])
})

运行实例

我们已经写完了模板编译代码,完整的使用流程如下:

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@2.7.16/dist/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    // 1. 模板字符串
    const template = `
      <div>
        <h1>恭喜!编译成功!</h1>
        <h1 v-if="true">随机数{{ Math.random() }}</h1>
        <h1 v-if="false">这里不会被展示</h1>
        <h2>
          <span>{{message}}</span>
          <span v-for="item in list">{{item}}</span>
        </h2>
      </div>
    `

    // 2. 三步编译:parse -> optimize -> generate
    // (函数实现见上文,这里省略)
    const ast = parse(template, {...})
    optimize(ast)
    const { render, staticRenderFns } = generate(ast)

    // 3. 使用生成的 render 函数创建 Vue 实例
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'Hello',
        list: ['我不吃', '饼干', '🍪'],
      },
      render: new Function(render),
      staticRenderFns: staticRenderFns.map(fn => new Function(fn)),
    })
  </script>
</body>
</html>

最终页面展示效果如下:

image.png

后记

以前看大佬ssh_晨曦时梦见兮的文章,特别喜欢他那种把问题深入浅出讲明白的文章。可是当我自己开始写的时候,才发现想写好真的好难。

有任何问题都可以在评论区提出,感谢大家看到这里。

高盛调查发现机构投资者对石油的看空程度接近10年来最高水平

2026年1月9日 07:30
高盛集团的一项调查发现,随着全球市场面临供应过剩,地缘政治因素正在推动机构投资者对原油的看空情绪接近过去10年来最强水平。根据周四发布的这项调查,在1100多名覆盖各类资产的高盛客户中,超过59%对原油持看空或略偏空观点。这使得市场情绪在可追溯至2016年1月份的月度调查数据中接近最悲观水平。(新浪财经)

谁是下任美联储主席,特朗普:心中已有决定

2026年1月9日 07:27
记者当地时间8日获悉,美国总统特朗普表示,关于美联储主席人选,他“心中已经有了决定”,但他没有透露最终人选。特朗普在7日晚表示,“我心里已经有了决定,我还没有和任何人谈过这件事。”当被问及他的首席经济顾问凯文·哈塞特时,特朗普称“我不想说”,但他形容哈塞特“无疑是我喜欢的人之一”。(央视新闻)

科学家破解豆科植物与根瘤菌的共生密码

2026年1月9日 07:23
记者从中国科学院分子植物科学卓越创新中心获悉,该中心科研团队首次成功解析了豌豆根瘤菌NodD蛋白与类黄酮类化合物(橙皮素)结合的高分辨复合物晶体结构,解析了NodD识别类黄酮类化合物的机制,并揭示NodD中决定信号识别特异性的关键结构元件。该研究成果1月9日在国际学术期刊《科学》在线发表。(央视新闻)

矿业巨头嘉能可与力拓重启合并谈判

2026年1月9日 07:19
嘉能可发布公告称,公司留意到近期媒体的猜测,并确认其正与力拓集团及力拓有限公司(统称“力拓”)就可能的业务合并事宜进行初步商谈,该合并可能涵盖力拓和嘉能可的部分或全部业务,其中包括力拓与嘉能可之间的全股合并。各方目前预期,任何合并交易将通过力拓以法院批准的安排计划方式收购嘉能可来实现。目前尚无法确定任何交易或要约的条款能否达成一致,即使达成一致,其具体条款或交易结构亦存在不确定性,届时将视情况另行发布通知。根据规定,力拓公司必须在2026年2月5日下午5点之前作出决定。近一年前,两家企业曾经进行涉及并购的磋商,但最后以失败告终。(财联社)

安踏拟收购德国彪马29%股权

2026年1月9日 07:13
安踏体育已向法国皮诺家族提出收购其所持德国彪马(Puma)29%股权的要约,交易若达成,安踏将取代皮诺家族成为彪马最大单一股东。根据公开资料,安踏体育此前已被列入对彪马发起收购要约的潜在竞购方之一。若推进竞购,安踏可能联合一家私募基金共同操作,模式类似此前收购亚玛芬体育的做法。其他潜在竞购方还包括李宁、日本亚瑟士、品牌管理巨头Authentic Brands Group以及私募基金CVC。消息人士称,目前安踏收购彪马的进展陷入停滞。(新浪财经)

大型科技公司在欧盟数字规则改革中或免受严格限制

2026年1月9日 07:08
据报道,知情人士表示,尽管电信公司呼吁加强监管,但谷歌、Meta、奈飞、微软和亚马逊在欧洲的数字规则改革中不会面临严厉监管。欧盟科技事务负责人将于1月20日公布这项名为《数字网络法案》(DNA)的规则改革方案,旨在提升欧洲的竞争力并促进对电信基础设施的投资。知情人士透露,科技巨头们将仅受自愿框架约束,而非电信运营商必须遵守的强制性规则。(界面)

OpenAI拟收购高管教练人工智能工具Convogo核心团队

2026年1月9日 07:08
据报道,人工智能巨头OpenAI将企业软件平台Convogo的核心团队收入麾下。Convogo平台主要面向高管教练、咨询师、人才发展负责人及人力资源团队,可助力其实现领导力评估与反馈报告的自动化,同时优化相关流程。OpenAI发言人表示,公司此次并未收购Convogo的知识产权或技术,而是通过招聘其团队,为自身“人工智能云业务”添砖加瓦。据知情人士透露,这笔交易为全股票交易,Convogo的三位联合创始人马特・库珀、埃文・凯特与迈克・吉莱特均将加入OpenAI。(新浪财经)

通用汽车四季度计提71亿美元专项支出,其中60亿由于收缩电车业务

2026年1月9日 07:07
通用汽车于周四宣布,公司将为去年四季度计提71亿美元专项支出,这笔支出与公司收缩电动汽车业务及推进业务重组直接相关。这家总部位于底特律的车企在公开申报文件中表示,上述支出包含两大核心部分:一是约60亿美元的电动车业务调整相关支出,此项支出的背景为当前电动车市场需求疲软;二是11亿美元的支出(其中包含5亿美元现金支出),该笔资金主要用于落实此前已公布的重组计划。(新浪财经)

美股三大指数收盘涨跌不一,大型科技股多数下跌

2026年1月9日 07:00
36氪获悉,1月8日收盘,美股三大指数涨跌不一,道指涨0.55%��纳指跌0.44%,标普500指数涨0.01%。大型科技股多数下跌,英特尔跌超3%,英伟达跌超2%,微软跌超1%,奈飞、苹果、Meta小幅下跌;谷歌、亚马逊、特斯拉涨超1%。热门中概股多数上涨,哔哩哔哩涨超6%,腾讯音乐、阿里巴巴涨超5%,小鹏汽车涨超3%,京东涨超2%;百度跌超3%,爱奇艺跌超2%,蔚来跌超1%。

每日一题-具有所有最深节点的最小子树🟡

2026年1月9日 00:00

给定一个根为 root 的二叉树,每个节点的深度是 该节点到根的最短距离

返回包含原始树中所有 最深节点最小子树

如果一个节点在 整个树 的任意节点之间具有最大的深度,则该节点是 最深的

一个节点的 子树 是该节点加上它的所有后代的集合。

 

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:
我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 5、3 和 2 包含树中最深的节点,但节点 2 的子树最小,因此我们返回它。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的节点为 2 ,有效子树为节点 2、1 和 0 的子树,但节点 2 的子树最小。

 

提示:

  • 树中节点的数量在 [1, 500] 范围内。
  • 0 <= Node.val <= 500
  • 每个节点的值都是 独一无二 的。

 

注意:本题与力扣 1123 重复:https://leetcode.cn/problems/lowest-common-ancestor-of-deepest-leaves

两种 O(n) 递归写法(Python/Java/C++/Go/JS)

作者 endlesscheng
2023年9月6日 08:01

前置题目236. 二叉树的最近公共祖先

视频讲解二叉树的最近公共祖先【基础算法精讲 12】

方法一:递归递归,有递有归

{:width=360px}

看上图(示例 1),这棵树的节点 $3,5,2$ 都是最深叶节点 $7,4$ 的公共祖先,但只有节点 $2$ 是最近的公共祖先。

上面视频中提到,如果我们要找的节点只在左子树中,那么最近公共祖先也必然只在左子树中。对于本题,如果左子树的最大深度比右子树的大,那么最深叶结点就只在左子树中,所以最近公共祖先也只在左子树中。

如果左右子树的最大深度一样呢?当前节点一定是最近公共祖先吗?

不一定。比如节点 $1$ 的左右子树最深叶节点 $0,8$ 的深度都是 $2$,但该深度并不是全局最大深度,所以节点 $1$ 并不能是答案。

根据以上讨论,正确做法如下:

  1. 递归这棵二叉树,同时维护全局最大深度 $\textit{maxDepth}$。
  2. 在「递」的时候往下传 $\textit{depth}$,用来表示当前节点的深度。
  3. 在「归」的时候往上传当前子树最深叶节点的深度。
  4. 设左子树最深叶节点的深度为 $\textit{leftMaxDepth}$,右子树最深叶节点的深度为 $\textit{rightMaxDepth}$。如果 $\textit{leftMaxDepth}=\textit{rightMaxDepth}=\textit{maxDepth}$,那么更新答案为当前节点。⚠注意:这并不代表我们立刻找到了答案,如果后面发现了更深的叶节点,答案还会更新。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        max_depth = -1  # 全局最大深度
        ans = None

        def dfs(node: Optional[TreeNode], depth: int) -> int:
            nonlocal ans, max_depth
            if node is None:
                max_depth = max(max_depth, depth)  # 维护全局最大深度
                return depth

            left_max_depth = dfs(node.left, depth + 1)  # 获取左子树最深叶节点的深度
            right_max_depth = dfs(node.right, depth + 1)  # 获取右子树最深叶节点的深度

            if left_max_depth == right_max_depth == max_depth:
                ans = node  # node 可能是答案

            return max(left_max_depth, right_max_depth)  # 当前子树最深叶节点的深度

        dfs(root, 0)
        return ans
class Solution {
    private int maxDepth = -1; // 全局最大深度
    private TreeNode ans;

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        dfs(root, 0);
        return ans;
    }

    private int dfs(TreeNode node, int depth) {
        if (node == null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        int leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        int rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }
}
class Solution {
public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        int max_depth = -1; // 全局最大深度
        TreeNode* ans;

        auto dfs = [&](this auto&& dfs, TreeNode* node, int depth) {
            if (node == nullptr) {
                max_depth = max(max_depth, depth); // 维护全局最大深度
                return depth;
            }

            int left_max_depth = dfs(node->left, depth + 1); // 获取左子树最深叶节点的深度
            int right_max_depth = dfs(node->right, depth + 1); // 获取右子树最深叶节点的深度

            if (left_max_depth == right_max_depth && left_max_depth == max_depth) {
                ans = node; // node 可能是答案
            }

            return max(left_max_depth, right_max_depth); // 当前子树最深叶节点的深度
        };

        dfs(root, 0);
        return ans;
    }
};
func subtreeWithAllDeepest(root *TreeNode) (ans *TreeNode) {
    maxDepth := -1 // 全局最大深度

    var dfs func(*TreeNode, int) int
    dfs = func(node *TreeNode, depth int) int {
        if node == nil {
            maxDepth = max(maxDepth, depth) // 维护全局最大深度
            return depth
        }

        leftMaxDepth := dfs(node.Left, depth+1) // 获取左子树最深叶节点的深度
        rightMaxDepth := dfs(node.Right, depth+1) // 获取右子树最深叶节点的深度

        if leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth {
            ans = node // node 可能是答案
        }

        return max(leftMaxDepth, rightMaxDepth) // 当前子树最深叶节点的深度
    }

    dfs(root, 0)
    return
}
var subtreeWithAllDeepest = function(root) {
    let maxDepth = -1; // 全局最大深度
    let ans = null;

    function dfs(node, depth) {
        if (node === null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        const leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        const rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth === rightMaxDepth && leftMaxDepth === maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }

    dfs(root, 0);
    return ans;
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

方法二:自底向上

也可以不用全局变量,而是把每棵子树都看成是一个「子问题」,即对于每棵子树,我们需要知道:

  • 这棵子树最深叶结点的深度。这里是指叶子在这棵子树内的深度,而不是在整棵二叉树的视角下的深度。相当于这棵子树的高度
  • 这棵子树的最深叶结点的最近公共祖先 $\textit{lca}$。

分类讨论:

  • 设子树的根节点为 $\textit{node}$,$\textit{node}$ 的左子树的高度为 $\textit{leftHeight}$,$\textit{node}$ 的右子树的高度为 $\textit{rightHeight}$。
  • 如果 $\textit{leftHeight} > \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 是左子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} < \textit{rightHeight}$,那么子树的高度为 $\textit{rightHeight} + 1$,$\textit{lca}$ 是右子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} = \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 就是 $\textit{node}$。反证法:如果 $\textit{lca}$ 在左子树中,那么 $\textit{lca}$ 不是右子树的最深叶结点的祖先,这不对;如果 $\textit{lca}$ 在右子树中,那么 $\textit{lca}$ 不是左子树的最深叶结点的祖先,这也不对;如果 $\textit{lca}$ 在 $\textit{node}$ 的上面,那就不符合「最近」的要求。所以 $\textit{lca}$ 只能是 $\textit{node}$。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        def dfs(node: Optional[TreeNode]) -> Tuple[int, Optional[TreeNode]]:
            if node is None:
                return 0, None

            left_height, left_lca = dfs(node.left)
            right_height, right_lca = dfs(node.right)

            if left_height > right_height:  # 左子树更高
                return left_height + 1, left_lca
            if left_height < right_height:  # 右子树更高
                return right_height + 1, right_lca
            return left_height + 1, node  # 一样高

        return dfs(root)[1]
class Solution {
    private record Pair(int height, TreeNode lca) {}

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        return dfs(root).lca;
    }

    private Pair dfs(TreeNode node) {
        if (node == null) {
            return new Pair(0, null);
        }

        Pair left = dfs(node.left);
        Pair right = dfs(node.right);

        if (left.height > right.height) { // 左子树更高
            return new Pair(left.height + 1, left.lca);
        }
        if (left.height < right.height) { // 右子树更高
            return new Pair(right.height + 1, right.lca);
        }
        return new Pair(left.height + 1, node); // 一样高
    }
}
class Solution {
    pair<int, TreeNode*> dfs(TreeNode* node) {
        if (node == nullptr) {
            return {0, nullptr};
        }

        auto [left_height, left_lca] = dfs(node->left);
        auto [right_height, right_lca] = dfs(node->right);

        if (left_height > right_height) { // 左子树更高
            return {left_height + 1, left_lca};
        }
        if (left_height < right_height) { // 右子树更高
            return {right_height + 1, right_lca};
        }
        return {left_height + 1, node}; // 一样高
    }

public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        return dfs(root).second;
    }
};
func dfs(node *TreeNode) (int, *TreeNode) {
    if node == nil {
        return 0, nil
    }

    leftHeight, leftLCA := dfs(node.Left)
    rightHeight, rightLCA := dfs(node.Right)

    if leftHeight > rightHeight { // 左子树更高
        return leftHeight + 1, leftLCA
    }
    if leftHeight < rightHeight { // 右子树更高
        return rightHeight + 1, rightLCA
    }
    return leftHeight + 1, node // 一样高
}

func subtreeWithAllDeepest(root *TreeNode) *TreeNode {
    _, lca := dfs(root)
    return lca
}
function dfs(node) {
    if (node === null) {
        return [0, null];
    }

    const [leftHeight, leftLca] = dfs(node.left);
    const [rightHeight, rightLca] = dfs(node.right);

    if (leftHeight > rightHeight) { // 左子树更高
        return [leftHeight + 1, leftLca];
    }
    if (leftHeight < rightHeight) { // 右子树更高
        return [rightHeight + 1, rightLca];
    }
    return [leftHeight + 1, node]; // 一样高
}

var subtreeWithAllDeepest = function(root) {
    return dfs(root, 0)[1];
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Java递归, O(n)一次遍历

作者 MapleStore
2021年4月15日 14:34

image.png

看了下下面的题解,除了官方题解用多个返回值只遍历了一次,其他题解大都反复计算深度,其实复杂度是O(nlogn)
本题解进行一次遍历得到结果

主要思想:

在一次遍历深度的过程中,找到左右子树深度都为最大值的节点记录下来

class Solution {
  private int maxDeep = Integer.MIN_VALUE;
  private TreeNode result;
  public TreeNode subtreeWithAllDeepest(TreeNode root) {
    maxDeep(root, 0);
    return result;
  }
  private int maxDeep(TreeNode node, int deep) {
    if (node == null) {
      return deep;
    }
    int left = maxDeep(node.left, deep+1);
    int right = maxDeep(node.right, deep+1);
    int currentMax = Math.max(left, right);
    maxDeep = Math.max(maxDeep, currentMax);
    if (left == maxDeep && right == maxDeep) {
      result = node;
    }
    return currentMax;
  }
}
❌
❌