普通视图
千问大免单活动延长3天
梅赛德斯-奔驰中国宣布重要人事任命:销售CEO段建军离职,李德思接任
LeetCode 25. K个一组翻转链表:两种解法详解+避坑指南
LeetCode 难度为 Hard 的经典链表题——25. K个一组翻转链表,这道题是链表翻转的进阶题,考察对链表指针操作的熟练度,也是面试中的高频考点,很多人会在“组内翻转”“组间连接”“边界处理”上踩坑。
今天不仅会讲解题目核心,还会对比两份不同思路的代码,分析它们的优缺点、避坑点,帮大家彻底吃透这道题,下次遇到直接秒解!
一、题目解读(清晰易懂版)
题目核心需求很明确,一句话概括:给一个链表,每k个节点当成一组,组内翻转;如果最后剩下的节点不足k个,就保持原样。
关键约束(必看,避坑前提):
-
k是正整数,且k ≤ 链表长度(不用考虑k大于链表长度的情况);
-
不能只改节点的值,必须实际交换节点(排除“偷巧”解法);
-
组间顺序不变,只有组内节点翻转(比如链表1->2->3->4,k=2,结果是2->1->4->3,不是4->3->2->1)。
示例辅助理解:
-
输入:head = [1,2,3,4,5], k = 2 → 输出:[2,1,4,3,5]
-
输入:head = [1,2,3,4,5], k = 3 → 输出:[3,2,1,4,5]
-
输入:head = [1,2], k = 2 → 输出:[2,1]
二、链表节点定义(题目给出,直接复用)
先贴出题目给出的ListNode定义,两份解法都基于这个结构,不用额外修改:
class ListNode {
val: number
next: ListNode | null
constructor(val?: number, next?: ListNode | null) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
}
三、两种解法详解对比
下面分别讲解两份代码(reverseKGroup_1 和 reverseKGroup_2),从思路、执行流程、优缺点三个维度拆解,帮大家看清两种思路的差异。
解法一:reverseKGroup_1(全局翻转+局部调整+回滚,新手易上手但需避坑)
1. 核心思路
这种思路的核心是「边遍历边全局翻转,每凑够k个节点,就调整一次组间连接;最后如果不足k个节点,再把这部分翻转回去」。
可以类比成:把链表当成一串珠子,从头开始逐个翻转(珠子顺序颠倒),每翻k个,就把这k个珠子“固定”到正确的位置(连接好前后组);如果最后剩的珠子不够k个,就把这几个珠子再翻回来,恢复原样。
2. 关键变量说明
-
dummy:虚拟头节点,避免处理头节点翻转的特殊情况(所有链表题的通用技巧);
-
preGroup:每组翻转的“前置节点”,负责连接上一组的尾和当前组的头;
-
prev:翻转节点时的“前驱节点”,记录当前节点的前一个节点(用于翻转指针);
-
curr:当前正在遍历、翻转的节点;
-
count:组内节点计数器,用于判断是否凑够k个节点。
3. 代码执行流程(以 head=[1,2,3,4], k=2 为例)
-
初始状态:dummy(0)->1->2->3->4,preGroup=dummy,prev=dummy,curr=1,count=0;
-
遍历curr=1:count≠2,翻转1(1.next=prev=dummy),prev=1,curr=2,count=1;
-
遍历curr=2:count≠2,翻转2(2.next=prev=1),prev=2,curr=3,count=2;
-
凑够k=2个节点:调整组间连接——preGroup.next=prev=2(dummy->2),原组头lastNode=1,1.next=curr=3(2->1->3);更新preGroup=1,prev=1,count=0;
-
继续遍历curr=3:重复步骤2-3,翻转3、4,凑够k=2个节点,调整连接(1->4,3.next=null);
-
循环结束,count=0,无不足k个的节点,返回dummy.next=2,最终结果2->1->4->3(正确)。
4. 优点&缺点
优点:思路直观,新手容易理解(只需要掌握“单个节点翻转”的基础操作,再加上计数和回滚);代码结构清晰,逐步骤执行,容易调试。
缺点:存在冗余逻辑(比如单独处理“最后一组刚好k个节点”的else if分支);过度使用空值断言(!),有潜在空指针风险;最后回滚步骤增加了少量时间开销(虽然时间复杂度还是O(n))。
5. 核心避坑点
-
避免链表环:翻转后必须及时调整组尾的next指针(lastNode.next=curr),否则会出现“dummy<->1”的环,触发运行错误;
-
回滚逻辑不能漏:如果最后剩余节点不足k个,必须把这部分翻转的节点再翻回来,否则会破坏原有顺序;
-
空值判断:preGroup.next不可能为null,可移除多余的空值判断,避免错误返回null。
解法二:reverseKGroup_2(先找组边界+组内单独翻转,最优解法)
这是更推荐的解法,也是面试中更常考的思路——「先找每组的边界(头和尾),确认够k个节点后,再单独翻转这组节点;组间连接直接通过边界节点处理,无需回滚」。
类比:还是一串珠子,先找到前k个珠子(确定组头和组尾),把这k个珠子单独翻转,再连接好前后珠子;再找下k个珠子,重复操作;如果找不到k个,就直接结束,不用再调整。
1. 关键变量说明(新增/差异变量)
-
groupTail:当前组的尾节点,通过移动k次找到,同时判断剩余节点是否够k个;
-
groupHead:当前组的头节点(翻转后会变成组尾);
-
nextGroupHead:下一组的头节点,提前记录,避免翻转后找不到下一组。
2. 代码执行流程(以 head=[1,2,3,4], k=2 为例)
-
初始状态:dummy(0)->1->2->3->4,preGroup=dummy;
-
找第一组边界:groupTail从preGroup开始移动2次,找到groupTail=2(确认够k个节点);记录groupHead=1,nextGroupHead=3;
-
单独翻转当前组(1->2):prev初始化为nextGroupHead=3,curr=groupHead=1;循环翻转,直到curr=nextGroupHead,翻转后变成2->1;
-
连接组间:preGroup.next=groupTail=2(dummy->2),preGroup更新为groupHead=1(下一组的前置节点);
-
找第二组边界:groupTail从preGroup=1移动2次,找到groupTail=4;记录groupHead=3,nextGroupHead=null;
-
单独翻转当前组(3->4),连接组间;
-
下一次找组边界:移动不足2次,count<k,直接返回dummy.next=2,结果2->1->4->3(正确)。
3. 优点&缺点
优点:逻辑更高效,无需回滚(提前判断节点数量,不足k个直接返回);无冗余分支,代码更简洁;指针操作更严谨,避免链表环和空指针风险;时间复杂度O(n),空间复杂度O(1),是最优解法。
缺点:对指针操作的熟练度要求更高,需要提前规划好“找边界-翻转-连接”的流程,新手可能需要多调试几次才能理解。
4. 核心避坑点
-
找组边界时,必须同时判断节点数量:移动k次后,如果groupTail.next不存在,说明不足k个节点,直接返回;
-
翻转组内节点时,prev初始化为nextGroupHead:这样翻转后,组尾(原groupHead)的next会自动指向nextGroupHead,无需额外调整;
-
preGroup更新为原groupHead:翻转后,原groupHead变成组尾,作为下一组的前置节点,保证组间连接正确。
四、两份代码对比总结
| 对比维度 | reverseKGroup_1 | reverseKGroup_2 |
|---|---|---|
| 核心思路 | 全局翻转+组间调整+不足k个回滚 | 先找组边界+组内单独翻转+无回滚 |
| 时间复杂度 | O(n)(回滚最多增加O(k),可忽略) | O(n)(最优,每个节点只遍历一次) |
| 空间复杂度 | O(1) | O(1) |
| 可读性 | 高,新手易理解 | 中等,需熟练掌握指针操作 |
| 适用场景 | 新手刷题、快速调试 | 面试、生产环境(最优解) |
| 潜在坑点 | 链表环、回滚遗漏、空值断言 | 组边界判断、prev初始化 |
五、刷题建议&拓展思考
1. 刷题建议
-
新手:先吃透 reverseKGroup_1,掌握“翻转+计数+回滚”的思路,熟练后再过渡到 reverseKGroup_2;
-
进阶:重点练习 reverseKGroup_2,尝试自己手写“找边界-翻转-连接”的流程,避免依赖模板;
-
调试技巧:遇到指针混乱时,画链表结构图(比如用草稿纸写出每个节点的next指向),逐步骤跟踪指针变化,比单纯看代码更高效。
2. 拓展思考(面试高频追问)
-
如果k可以大于链表长度,该如何修改代码?(提示:在找组边界时,判断count是否等于链表长度,不足则不翻转);
-
如何用递归实现K个一组翻转链表?(提示:递归终止条件是剩余节点不足k个,递归逻辑是翻转当前组,再递归翻转下一组);
-
如果要求“每k个节点一组翻转,不足k个节点时全部翻转”,该如何修改?(提示:移除回滚逻辑,或不判断节点数量,直接翻转)。
六、最终优化版代码(推荐面试使用)
基于 reverseKGroup_2 优化,移除空值断言,增加防御性判断,代码更健壮、简洁,适配面试场景:
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
if (k === 1 || !head || !head.next) return head;
const dummy = new ListNode(0, head);
let preGroup = dummy; // 每组翻转的前置节点
let count = 0;
while (true) {
// 第一步:找组尾,判断剩余节点是否够k个
let groupTail = preGroup;
count = 0;
while (count < k && groupTail.next) {
groupTail = groupTail.next;
count++;
}
if (count < k) return dummy.next; // 不足k个,直接返回
// 第二步:记录关键节点
const groupHead = preGroup.next;
const nextGroupHead = groupTail.next;
// 第三步:组内翻转
let prev: ListNode | null = nextGroupHead;
let curr = groupHead;
while (curr !== nextGroupHead) {
const next = curr?.next;
if (curr) curr.next = prev;
prev = curr;
curr = next;
}
// 第四步:组间连接
preGroup.next = groupTail;
preGroup = groupHead!;
}
}
七、总结
LeetCode 25题的核心是「组内翻转+组间连接」,两种解法的本质都是通过指针操作实现,但思路的高效性有差异。
无论哪种解法,都要记住三个核心要点:① 用虚拟头节点简化头节点处理;② 明确每组的边界(头、尾、下一组头);③ 翻转时避免链表环和空指针。
刷题不是背代码,而是理解思路、掌握技巧。建议大家多调试、多画图,熟练掌握指针操作,下次遇到类似的链表翻转题(比如两两翻转、指定区间翻转),就能举一反三、轻松应对!
迪拜2025年国际游客近2000万,中国游客达86万
Claude Code 作者再次分享 Anthropic 内部团队使用技巧
大家好,我是 Immerse,一名独立开发者、内容创作者、AGI 实践者。
关注公众号:沉浸式AI,获取最新文章(更多内容只在公众号更新)
个人网站:yaolifeng.com 也同步更新。
转载请在文章开头注明出处和版权信息。
我会在这里分享关于编程、独立开发、AI干货、开源、个人思考等内容。
如果本文对您有所帮助,欢迎动动小手指一键三连(点赞、评论、转发),给我一些支持和鼓励,谢谢!
Boris 又发了一份 Anthropic 内部的 Claude Code 使用心得。
看完觉得挺实用,记录几条:
1. 多开 worktree 同时跑 3-5 个 git worktree,每个开一个独立会话。团队里公认这个最提效。Boris 自己习惯用 git checkout,但大部分人更爱 worktree。
2. 复杂任务先规划 遇到复杂活儿就开 plan mode。可以让一个 Claude 写计划,另一个当幕僚审查。跑偏了就切回去重新规划。验证环节也会专门进计划模式。
3. 错误后更新 CLAUDE.md 每次纠错完都加一句:"更新你的 CLAUDE.md,别再犯同样的错。"反复迭代到错误率明显降下来。
4. 自建 Skills 库 把常用操作做成 Skills 提交到 git,各项目复用。一天做两次以上的事就该做成 Skills。
5. 让 Claude 自己修 bug 接入 Slack MCP,把 bug 讨论帖扔给 Claude,说一句"修它"就行。或者直接"去修失败的 CI",不用管细节。
6. 提高提示词质量 试试"严格审查这些改动,测试不过不准建 PR",让 Claude 当审查员。或者"证明给我看这能跑通",让它对比 main 和功能分支的差异。
7. 追求更优方案 碰到平庸的修复就说:"基于现在掌握的信息,废掉这个方案,实现更优雅的。"任务前写详细规格,减少歧义。描述越具体,输出越好。
8. 终端配置 团队在用 Ghostty 终端,支持同步渲染、24 位色彩和完整 Unicode。用 /statusline 自定义状态栏显示上下文用量和 git 分支。给标签页做颜色编码和命名,一个任务一个标签页。
9. 语音输入 说话比打字快三倍,提示词也会详细很多。macOS 连按两次 fn 就能开启。
10. 用子代理 想让 Claude 多花点算力就加"use subagents"。把任务分给子代理,主代理的上下文窗口保持干净。
详情:x.com/bcherny/status/2017742741636321619 x
gsap 配置解读 --5
什么是ScrollTo
<header>
<h1>案例 29:ScrollTo 平滑滚动</h1>
<button id="to-second">滚动到第二屏</button>
</header>
<section>
<div class="panel">第一屏内容</div>
</section>
<section id="second">
<div class="panel">第二屏内容</div>
</section>
<section>
<div class="panel">第三屏内容</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollToPlugin.min.js"></script>
<script>
const button = document.querySelector("#to-second");
// 注册 ScrollToPlugin
gsap.registerPlugin(ScrollToPlugin);
button.addEventListener("click", () => {
gsap.to(window, {
duration: 1,
scrollTo: "#second",
ease: "power2.out"
});
});
</script>
ScrollToPlugin 是 GSAP(GreenSock Animation Platform) 提供的一个轻量级但非常实用的插件,用于实现 平滑、可控的页面滚动动画——无论是滚动到页面某个元素、指定坐标,还是精确控制滚动行为。
📌 简单定义:
ScrollToPlugin让你用 GSAP 的动画语法(如duration、ease)驱动浏览器窗口或任意可滚动容器,平滑滚动到目标位置。
它解决了原生 window.scrollTo() 只能“瞬间跳转”或简单 behavior: 'smooth' 缺乏控制的问题。
✅ 核心能力:
1. 滚动到多种目标
// 滚动到元素(通过选择器或 DOM 节点)
scrollTo: "#second"
scrollTo: document.querySelector(".footer")
// 滚动到具体坐标
scrollTo: { y: 500 } // 垂直滚动到 500px
scrollTo: { x: 200, y: 300 } // 水平 + 垂直
// 滚动到页面顶部/底部
scrollTo: { y: "top" }
scrollTo: { y: "bottom" }
// 滚动到元素并预留偏移(如避开固定导航栏)
scrollTo: { y: "#section", offsetY: 80 }
2. 完全控制动画体验
-
duration: 滚动持续时间(秒) -
ease: 缓动函数(如"power2.out"、"expo.inOut") - 可暂停、反向、加入时间轴(Timeline)
3. 支持任意可滚动容器
不仅限于 window,也可用于 <div style="overflow: auto"> 等局部滚动区域:
gsap.to(scrollableDiv, {
duration: 1,
scrollTo: { y: 1000 }
});
🔧 在你的代码中:
gsap.to(window, {
duration: 1,
scrollTo: "#second", // 平滑滚动到 id="second" 的 <section>
ease: "power2.out" // 先快后慢的缓动效果
});
点击按钮后:
- 页面不会“瞬间跳转”到第二屏;
- 而是用 1 秒时间,以 优雅的缓动曲线 滚动到
#second元素的顶部; - 用户体验更自然、专业。
🌟 典型应用场景:
| 场景 | 示例 |
|---|---|
| 导航跳转 | 点击菜单项平滑滚动到对应章节 |
| “回到顶部”按钮 | 带缓动的返回顶部动画 |
| 表单错误定位 | 提交失败时滚动到第一个错误字段 |
| 交互式故事页 | 按钮触发滚动到下一情节 |
| 局部滚动容器 | 在聊天窗口中自动滚动到底部 |
⚙️ 高级选项(常用):
scrollTo: {
y: "#target",
offsetX: 0, // 水平偏移
offsetY: 60, // 垂直偏移(常用于避开固定头部)
autoKill: true // 用户手动滚动时自动中断动画(默认 true)
}
🆚 对比原生方案:
| 方式 | 控制力 | 缓动 | 中断处理 | 兼容性 |
|---|---|---|---|---|
window.scrollTo({ behavior: 'smooth' }) |
低 | 仅线性 | 差 | 现代浏览器 |
ScrollToPlugin |
高 | 任意 GSAP 缓动 | 智能中断 | 全浏览器(含 IE11) |
⚠️ 注意事项:
- 必须注册插件:
gsap.registerPlugin(ScrollToPlugin) - 目标元素必须已存在于 DOM 中
- 如果结合
ScrollSmoother(平滑滚动容器),需使用其 API 而非直接操作window
📚 官方文档:
✅ 总结:
ScrollToPlugin是 GSAP 中实现“专业级页面导航动画”的标准工具——它用极简的代码,赋予滚动行为电影般的流畅感和精准控制,是提升网站交互质感的必备插件。
什么是SplitText
<div class="card">
<h1 id="headline">SplitText 可以拆分文字做逐字动画</h1>
<button id="play">逐字出现</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/SplitText.min.js"></script>
<script>
const headline = document.querySelector("#headline");
const playButton = document.querySelector("#play");
// 注册 SplitText 插件
gsap.registerPlugin(SplitText);
let split;
playButton.addEventListener("click", () => {
if (split) {
split.revert();
}
// 将文字拆分为字符
split = new SplitText(headline, { type: "chars" });
gsap.from(split.chars, {
opacity: 0,
y: 20,
duration: 0.6,
ease: "power2.out",
stagger: 0.04
});
});
</script>
SplitText 是 GSAP(GreenSock Animation Platform) 提供的一个强大工具(虽然叫“插件”,但实际是一个独立的实用类),用于将 HTML 文本智能地拆分为可单独动画的 <span> 元素,从而实现精细的逐字、逐词或逐行动画效果。
📌 简单定义:
SplitText能把一段普通文字(如<h1>Hello</h1>)自动转换成包裹在<span>中的字符、单词或行,让你可以用 GSAP 对每个部分做独立动画。
例如:
<!-- 原始 -->
<h1 id="headline">你好</h1>
<!-- SplitText({ type: "chars" }) 处理后 -->
<h1 id="headline">
<span class="char">你</span>
<span class="char">好</span>
</h1>
✅ 核心功能:三种拆分模式
| 模式 | 说明 | 生成结构 |
|---|---|---|
type: "chars" |
拆分为单个字符(包括中文、英文、标点) | 每个字一个 <span class="char">
|
type: "words" |
拆分为单词(以空格/标点分隔) | 每个词一个 <span class="word">
|
type: "lines" |
拆分为视觉上的行(根据实际换行) | 每行外层包 <div class="line">
|
💡 也可组合使用:
type: "words, chars"→ 先分词,再把每个词拆成字。
split = new SplitText(headline, { type: "chars" });
gsap.from(split.chars, {
opacity: 0,
y: 20,
duration: 0.6,
ease: "power2.out",
stagger: 0.04 // 每个字符延迟 0.04 秒启动
});
- 点击按钮时,标题文字被拆成单个字符;
- 每个字符从下方 20px、透明的状态,依次向上淡入;
- 形成“逐字打字机”或“文字飞入”的经典动效。
⚠️ 注意:每次点击前调用
split.revert()是为了还原原始 HTML 结构,避免重复嵌套<span>导致样式错乱。
🌟 为什么需要 SplitText?
如果不使用它,手动写 <span> 包裹每个字:
- 繁琐:尤其对动态内容或 CMS 内容不现实;
- 破坏语义:影响 SEO 和可访问性(屏幕阅读器);
- 难以维护。
而 SplitText:
- 非破坏性:原始文本保持不变,仅运行时包装;
- 智能处理:正确保留 HTML 标签、空格、换行、内联样式;
-
支持复杂排版:包括多行、响应式断行(
lines模式会监听 resize)。
🛠️ 高级特性:
- 保留原始样式:即使文字有 CSS 动画、颜色、字体,拆分后依然生效。
- 与 ScrollTrigger 结合:实现“滚动到此处时逐字出现”。
- 支持 SVG 文本(需额外配置)。
-
可自定义包裹标签:默认
<span>,也可设为<div>等。
⚠️ 注意事项:
-
不是免费插件:在 GSAP 3 中,
SplitText属于 Club 会员专属功能(可试用,但商业项目需授权)。 -
不要重复拆分:务必在重新拆分前
revert(),否则会嵌套多层<span>。 - 对 SEO 友好:因为原始 HTML 不变,搜索引擎仍能读取完整文本。
📚 官方文档:
✅ 总结:
SplitText是 GSAP 中实现“高级文字动画”的基石工具——它将枯燥的文本转化为可编程的动画单元,让逐字淡入、弹跳、飞入等效果变得简单、可靠且专业,广泛应用于官网、片头、交互叙事等场景。
什么是TextPlugin
<div class="card">
<h1>案例 31:TextPlugin 数字滚动</h1>
<p>让文本从 0 变化到目标值。</p>
<div class="counter" id="counter">0</div>
<button id="play">开始计数</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/TextPlugin.min.js"></script>
<script>
const counter = document.querySelector("#counter");
const playButton = document.querySelector("#play");
// 注册 TextPlugin
gsap.registerPlugin(TextPlugin);
const tween = gsap.to(counter, {
duration: 1.6,
text: "1280",
ease: "power2.out",
paused: true
});
playButton.addEventListener("click", () => {
counter.textContent = "0";
tween.restart();
});
</script>
TextPlugin 是 GSAP(GreenSock Animation Platform) 提供的一个轻巧但非常实用的插件,专门用于对 DOM 元素的文本内容进行动画化更新。它最经典的应用就是实现 “数字滚动计数器” 效果(如从 0 平滑变化到 1280),但也支持普通文本的渐变替换。
📌 简单定义:
TextPlugin能让元素的textContent从一个值“动画过渡”到另一个值——对于数字,它会逐帧递增/递减;对于文字,它可模拟打字、随机字符替换等效果。
✅ 核心功能:
1. 数字滚动(最常用)
gsap.to(element, {
duration: 2,
text: "1000" // 自动从当前数字(如 "0")滚动到 1000
});
- 自动识别数字并进行数值插值;
- 支持整数、小数、带千分位格式(需配合
delimiter); - 可设置前缀/后缀(如
$、%)。
2. 文本替换动画
gsap.to(element, {
text: "Hello World",
duration: 1.5
});
- 默认行为:直接替换(无中间动画);
- 但配合
delimiter或自定义逻辑,可实现打字机、乱码过渡等(不过复杂文本动画更推荐ScrambleTextPlugin)。
gsap.to(counter, {
duration: 1.6,
text: "1280", // 目标文本
ease: "power2.out",
paused: true
});
- 初始文本是
"0"; - 点击按钮后,
TextPlugin会:- 解析
"0"和"1280"都是有效数字; - 在 1.6 秒内,将文本内容从
0 → 1 → 2 → ... → 1280逐帧更新; - 视觉上形成“数字飞速增长”的计数器效果。
- 解析
💡 注意:每次播放前重置
counter.textContent = "0"是为了确保动画从起点开始。
⚙️ 常用配置选项(通过 text 对象):
gsap.to(element, {
text: {
value: "¥1,280", // 目标值
delimiter: ",", // 千分位分隔符
prefix: "¥", // 前缀(也可直接写在 value 里)
suffix: " 元", // 后缀
padSpace: true // 保持文本长度一致(防跳动)
},
duration: 2
});
🌟 典型应用场景:
| 场景 | 示例 |
|---|---|
| 数据看板 | 用户数、销售额、点赞数的动态增长 |
| 加载进度 | “加载中... 78%” |
| 倒计时/计时器 | 活动剩余时间、秒表 |
| 游戏得分 | 分数变化动画 |
| 简单文本切换 | 状态提示(“成功” → “完成”) |
🆚 对比其他方案:
| 方法 | 数字滚动 | 文本动画 | 精确控制 | 性能 |
|---|---|---|---|---|
手动 setInterval
|
✅ | ❌ | 低 | 一般 |
| CSS + JS 拼接 | ⚠️ 复杂 | ⚠️ 有限 | 低 | 一般 |
TextPlugin |
✅✅✅ | ✅ | 高(GSAP 时间轴) | 优 |
⚠️ 注意事项:
-
只作用于
textContent,不会影响 HTML 标签(即不能插入<strong>等); - 如果起始或目标文本不是纯数字,则直接替换(无滚动);
- 要实现更炫的文字扰动(如乱码过渡),应使用
ScrambleTextPlugin; -
免费可用:
TextPlugin是 GSAP 的标准免费插件(无需会员)。
📚 官方文档:
✅ 总结:
TextPlugin是 GSAP 中实现“数字计数器动画”的首选工具——它用一行代码就能将静态数字变成动态增长的视觉焦点,简单、高效、且完全集成于 GSAP 动画生态系统。
什么是EasePack
<div class="card">
<h1>案例 32:EasePack 特殊缓动</h1>
<p>RoughEase / SlowMo / ExpoScaleEase 都在 EasePack 中。</p>
<div class="row">
<div>
<div class="lane">
<div class="ball" id="ballA"></div>
</div>
<div class="label">RoughEase</div>
</div>
<div>
<div class="lane">
<div class="ball" id="ballB"></div>
</div>
<div class="label">SlowMo</div>
</div>
<div>
<div class="lane">
<div class="ball" id="ballC"></div>
</div>
<div class="label">ExpoScaleEase</div>
</div>
</div>
<button id="play">播放缓动</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<!-- RoughEase, ExpoScaleEase and SlowMo are all included in the EasePack file -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/EasePack.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomEase.min.js"></script>
<!-- CustomBounce requires CustomEase -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomBounce.min.js"></script>
<!-- CustomWiggle requires CustomEase -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomWiggle.min.js"></script>
<script>
const ballA = document.querySelector("#ballA");
const ballB = document.querySelector("#ballB");
const ballC = document.querySelector("#ballC");
const playButton = document.querySelector("#play");
// 预设三个缓动
const rough = RoughEase.ease.config({
strength: 1.5,
points: 20,
template: Power1.easeInOut,
randomize: true
});
const slowMo = SlowMo.ease.config(0.7, 0.7, false);
const expoScale = ExpoScaleEase.config(1, 3);
const timeline = gsap.timeline({ paused: true });
timeline.to(ballA, { y: 100, duration: 1.2, ease: rough }, 0);
timeline.to(ballB, { y: 100, duration: 1.2, ease: slowMo }, 0);
timeline.to(ballC, { y: 100, duration: 1.2, ease: expoScale }, 0);
playButton.addEventListener("click", () => {
gsap.set([ballA, ballB, ballC], { y: 0 });
timeline.restart();
});
</script>
RoughEase、SlowMo 和 ExpoScaleEase 是 GSAP(GreenSock Animation Platform) 中三个非常有特色的高级缓动函数(easing functions),它们都包含在 EasePack 插件中。它们超越了传统的“入/出”缓动(如 easeInOut),提供了更具创意和物理感的动画节奏。
下面分别解释它们的作用和适用场景:
1. 🌀 RoughEase —— “抖动式”缓动
✅ 作用:
模拟不规则、随机抖动的运动效果,常用于表现:
- 手绘感、草图风格
- 震动、故障、不稳定状态
- 卡通式的“弹跳后晃动”
🔧 核心参数(通过 .config() 设置):
const rough = RoughEase.ease.config({
strength: 1.5, // 抖动强度(0~2,默认 1)
points: 20, // 抖动点数量(越多越密集)
template: Power1.easeInOut, // 基础缓动曲线(决定整体趋势)
randomize: true // 是否每次播放随机(true=更自然)
});
🎯 在你的代码中:
- 小球 A 下落时会上下轻微抖动,不是平滑移动,而是像“被手抖着拉下来”。
💡 适合:游戏中的受击反馈、加载失败提示、趣味 UI。
2. 🐢 SlowMo —— “慢动作中心”缓动
✅ 作用:
让动画在中间阶段变慢,两端加速,形成“慢镜头”效果。
特别适合强调某个关键状态(如悬停、高亮、停顿)。
🔧 核心参数:
const slowMo = SlowMo.ease.config(
linearRatio, // 中间匀速部分占比(0~1)
power, // 两端加速强度(0=线性,1=强缓出)
yoyoMode // 是否用于往返动画(true=对称)
);
例如:SlowMo.ease.config(0.7, 0.7, false)
→ 动画 70% 的时间以近似匀速缓慢进行,开头和结尾快速过渡。
- 小球 B 下落时,大部分时间缓慢移动,只在开始和结束瞬间加速,仿佛“优雅降落”。
💡 适合:产品展示、LOGO 入场、需要突出中间状态的动画。
3. 📏 ExpoScaleEase —— “指数缩放”缓动
✅ 作用:
实现基于比例(scale)或指数增长/衰减的非线性缓动。
常用于:
- 缩放动画(从 1x 到 10x)
- 音量/亮度/透明度等对数感知属性
- 模拟真实世界的指数变化(如声音衰减、光强)
🔧 核心参数:
const expoScale = ExpoScaleEase.config(startValue, endValue);
- 它会将动画值从
startValue到endValue按指数曲线映射。 - 通常配合
scale、opacity或自定义属性使用。
🎯 虽然用于 y,但效果仍体现非线性:
- 小球 C 的下落速度先快后慢(或反之,取决于范围),但变化是非线性的,比
power2更“陡峭”。
💡 更典型用法:
gsap.to(circle, { scale: 5, ease: ExpoScaleEase.config(1, 5) // 从 1 倍到 5 倍的指数缩放 });
💡 适合:放大镜效果、爆炸扩散、雷达扫描、声波可视化。
🆚 对比总结:
| 缓动类型 | 视觉特点 | 典型用途 |
|---|---|---|
RoughEase |
随机抖动、不规则 | 故障风、手绘感、震动反馈 |
SlowMo |
中间慢、两头快 | 强调关键帧、优雅停顿 |
ExpoScaleEase |
指数级加速/减速 | 缩放、对数感知属性、物理模拟 |
⚠️ 注意事项:
- 这些缓动都来自
EasePack,需单独引入(如你代码中已做); - 它们可以像普通
ease一样用在任何 GSAP 动画中; - 结合
Timeline可创建复杂节奏组合。
📚 官方文档:
- Ease Visualizer(可交互体验):greensock.com/ease-visual…
- EasePack 文档:greensock.com/docs/v3/Eas…
✅ 总结:
RoughEase、SlowMo、ExpoScaleEase是 GSAP 赋予动画“性格”的秘密武器——它们让运动不再机械,而是充满随机性、戏剧性或物理真实感,是打造高级交互动效的关键工具。
什么是 CustomEase
<div class="card">
<h1>案例 33:CustomEase 自定义缓动</h1>
<p>用贝塞尔曲线定义缓动曲线。</p>
<div class="track">
<div class="block" id="block"></div>
</div>
<button id="play">播放自定义缓动</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomEase.min.js"></script>
<script>
const block = document.querySelector("#block");
const playButton = document.querySelector("#play");
// 注册 CustomEase
gsap.registerPlugin(CustomEase);
// 创建一个自定义缓动曲线
CustomEase.create("myEase", "0.25,0.1,0.25,1");
const tween = gsap.to(block, {
x: 470,
duration: 1.4,
ease: "myEase",
paused: true
});
playButton.addEventListener("click", () => {
tween.restart();
});
</script>
CustomEase 是 GSAP(GreenSock Animation Platform) 提供的一个强大插件,允许你通过自定义贝塞尔曲线(cubic-bezier)来创建完全个性化的缓动函数(easing function),从而精确控制动画的速度变化节奏。
📌 简单定义:
CustomEase让你像在 CSS 或设计工具中那样,用 4 个控制点定义一条缓动曲线,并将其注册为可复用的 GSAP 缓动名称,用于任何动画。
它打破了内置缓动(如 power2.inOut、elastic)的限制,实现电影级、品牌专属或物理拟真的运动节奏。
✅ 核心原理:贝塞尔曲线
缓动曲线本质是一条 三次贝塞尔曲线(Cubic Bezier),由 4 个点定义:
- 起点固定为
(0, 0) - 终点固定为
(1, 1) - 中间两个控制点
(x1, y1)和(x2, y2)决定曲线形状
在 CustomEase 中,你只需提供这 4 个数值(按顺序):
" x1, y1, x2, y2 "
例如你的代码:
CustomEase.create("myEase", "0.25,0.1,0.25,1");
表示:
- 控制点 1:
(0.25, 0.1) - 控制点 2:
(0.25, 1)
这条曲线的特点是:启动非常快(y1 很低),然后突然减速并平稳结束,形成一种“急冲后刹车”的效果。
🔧 使用步骤:
-
引入插件
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomEase.min.js"></script> -
注册自定义缓动
CustomEase.create("myEase", "0.25,0.1,0.25,1");- 第一个参数:缓动名称(字符串,如
"myEase") - 第二个参数:贝塞尔控制点(4 个 0~1 之间的数字,用逗号分隔)
- 第一个参数:缓动名称(字符串,如
-
在动画中使用
gsap.to(block, { x: 470, duration: 1.4, ease: "myEase" // 直接使用注册的名称 });
🌟 优势 vs 其他方式:
| 方式 | 灵活性 | 可视化 | 复用性 | 性能 |
|---|---|---|---|---|
CSS cubic-bezier()
|
✅ | ✅(开发者工具) | ❌(需重复写) | ✅ |
| 手动计算进度 | ❌ | ❌ | ❌ | ⚠️ |
CustomEase |
✅✅✅ | ✅(配合 GSAP 工具) | ✅✅✅(全局注册) | ✅✅(预计算优化) |
💡
CustomEase会预计算并缓存曲线数据,运行时性能极高,适合复杂动画。
🛠️ 如何获取贝塞尔值?
-
使用 GSAP 官方工具(推荐!)
👉 GSAP Ease Visualizer- 拖动控制点实时预览动画
- 自动生成
CustomEase代码
-
从 CSS 复制
如果你在 CSS 中写了:transition: all 1s cubic-bezier(0.25, 0.1, 0.25, 1);那么值就是
"0.25,0.1,0.25,1" -
从 Figma / After Effects 导出
许多设计工具支持导出贝塞尔缓动参数。
🎨 典型应用场景:
| 效果 | 贝塞尔示例 | 用途 |
|---|---|---|
| 弹性回弹 | "0.68,-0.55,0.27,1.55" |
按钮点击反馈 |
| 缓入缓出加强版 | "0.33,0,0.67,1" |
平滑过渡 |
| 快速启动+慢速结束 |
"0.25,0.1,0.25,1"(你的例子) |
强调终点状态 |
| 延迟启动 | "0.5,0,0.75,0" |
悬停后才开始动画 |
⚠️ 注意事项:
- 所有数值必须在 0 到 1 之间(超出会导致不可预测行为);
- 注册一次后,可在整个项目中复用(如
"brandBounce"、"softEase"); -
免费可用:
CustomEase是 GSAP 的标准插件(无需 Club 会员); - 若需更复杂曲线(如多段),可结合
CustomWiggle或CustomBounce(它们依赖CustomEase)。
📚 官方资源:
✅ 总结:
CustomEase是 GSAP 中实现“精准运动设计”的终极工具——它把缓动从“选择预设”升级为“自由创作”,让开发者和设计师能用同一套语言定义品牌专属的动画节奏,是打造高端用户体验的核心技术之一。
React 性能优化双子星:深入、全面解析 useMemo 与 useCallback
引言
在现代 React 应用开发中,随着组件逻辑日益复杂、状态管理愈发庞大,性能问题逐渐成为开发者绕不开的话题。幸运的是,React 提供了两个强大而精巧的 Hooks —— useMemo 和 useCallback,它们如同“缓存魔法”,帮助我们在不牺牲可读性的前提下,显著提升应用性能。
本文将结合完整代码示例,逐行解析、对比说明、深入原理,带你彻底掌握 useMemo 与 useCallback 的使用场景、工作机制、常见误区以及最佳实践。文章内容力求全面、准确、生动有趣,并严格保留原始代码一字不变,确保你既能理解理论,又能直接复用实战。
一、为什么需要 useMemo 和 useCallback?
1.1 React 函数组件的“重运行”特性
在 React 中,每当组件的状态(state)或 props 发生变化时,整个函数组件会重新执行一遍。这意味着:
- 所有变量都会重新声明;
- 所有函数都会重新定义;
- 所有计算逻辑都会重新跑一次。
这本身是 React 响应式更新机制的核心,但也会带来不必要的开销。
💡 关键洞察:
“组件函数重新运行” ≠ “DOM 重新渲染”。
React 会通过 Virtual DOM diff 算法决定是否真正更新 DOM。
但昂贵的计算或子组件的无谓重渲染,仍可能拖慢应用。
二、useMemo:为“昂贵计算”穿上缓存外衣
2.1 什么是“昂贵计算”?
看这段代码:
// 昂贵的计算
function slowSum(n) {
console.log('计算中...')
let sum = 0
for(let i = 0; i < n*10000; i++){
sum += i
}
return sum
}
这个 slowSum 函数执行了 n * 10000 次循环!如果 n=100,就是一百万次加法。在每次组件重渲染时都调用它,用户界面可能会卡顿。
2.2 不用 useMemo 的后果
假设我们这样写:
const result = slowSum(num); // ❌ 每次渲染都重新计算!
那么,即使你只是点击了 count + 1 按钮(与 num 无关),slowSum 依然会被执行!因为整个 App 函数重新运行了。
2.3 useMemo 如何拯救性能?
React 提供 useMemo 来记忆(memoize)计算结果:
const result = useMemo(() => {
return slowSlow(num)
}, [num])
✅ 工作原理:
第一次渲染:执行函数,缓存结果。
后续渲染:检查依赖项
[num]是否变化。
- 如果
num没变 → 直接返回缓存值,不执行函数体。- 如果
num变了 → 重新执行函数,更新缓存。
2.4 完整上下文中的 useMemo 使用
export default function App(){
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)
const [keyword, setKeyword] = useState('')
const list = ['apple','banana', 'orange', 'pear']
// ✅ 仅当 keyword 改变时才重新过滤
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword))
}, [keyword])
// ✅ 仅当 num 改变时才重新计算 slowSum
const result = useMemo(() => {
return slowSum(num)
}, [num])
return (
<div>
<p>结果: {result}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
<input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
{
filterList.map(item => (
<li key={item}>{item}</li>
))
}
</div>
)
}
🔍 重点观察:
点击 “count + 1” 时:
slowSum不会执行(因为num没变);filterList不会重新计算(因为keyword没变);- 控制台不会打印 “计算中...” 或隐含的 “filter执行”。
这就是
useMemo带来的精准缓存!
2.5 关于 includes 和 filter 的小贴士
-
"apple".includes("")确实返回true(空字符串是任何字符串的子串); -
list.filter(...)返回的是一个新数组,即使结果为空(如[]),它也是一个新的引用。
⚠️ 正因如此,如果不使用
useMemo,每次渲染都会生成一个新数组引用,可能导致依赖该数组的子组件误判为 props 变化而重渲染!
三、useCallback:为“回调函数”打造稳定身份
3.1 问题起源:函数是“新”的!
在 JavaScript 中,每次函数定义都会创建一个新对象:
// 每次 App 重运行,handleClick 都是一个全新函数!
const handleClick = () => { console.log('click') }
即使函数体完全一样,handleClick !== previousHandleClick。
3.2 子组件为何“无辜重渲染”?
看这段代码:
const Child = memo(({count, handleClick}) => {
console.log('child重新渲染')
return (
<div onClick={handleClick}>
<h1>子组件 count: {count}</h1>
</div>
)
})
-
memo的作用:浅比较 props,若没变则跳过渲染。 - 但每次父组件重渲染,
handleClick都是新函数 → props 引用变了 →memo失效 → 子组件重渲染!
即使你只改了 num,Child 也会重渲染,尽管它只关心 count!
3.3 useCallback 的解决方案
useCallback 本质上是 useMemo 的语法糖,专用于缓存函数:
const handleClick = useCallback(() => {
console.log('click')
}, [count])
✅ 效果:
- 只要
count不变,handleClick的引用就保持不变;Child的 props 引用未变 →memo生效 → 跳过重渲染!
3.4 完整 useCallback 示例
import {
useState,
memo,
useCallback
} from 'react'
const Child = memo(({count, handleClick}) => {
console.log('child重新渲染')
return (
<div onClick={handleClick}>
<h1>子组件 count: {count}</h1>
</div>
)
})
export default function App(){
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
// ✅ 缓存函数,依赖 count
const handleClick = useCallback(() => {
console.log('click')
}, [count])
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<p>num: {num}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
<Child count={count} handleClick={handleClick} />
</div>
)
}
🔍 行为验证:
- 点击 “num + 1”:
Child不会打印 “child重新渲染”;- 点击 “count + 1”:
Child会重渲染(因为count和handleClick都变了);- 如果
handleClick不依赖count(依赖项为[]),则只有count变化时Child才重渲染。
四、useMemo vs useCallback:一张表说清区别
| 特性 | useMemo |
useCallback |
|---|---|---|
| 用途 | 缓存任意值(数字、数组、对象等) | 缓存函数 |
| 本质 | useMemo(fn, deps) |
useMemo(() => fn, deps) 的简写 |
| 典型场景 | 昂贵计算、过滤/映射大数组、创建复杂对象 | 传递给 memo 子组件的事件处理器 |
| 返回值 | 函数执行的结果 | 函数本身 |
| 错误用法 | 用于无副作用的纯计算 | 用于依赖外部变量但未声明依赖 |
💡 记住:
useCallback(fn, deps)≡useMemo(() => fn, deps)
五、常见误区与最佳实践
❌ 误区1:到处使用 useMemo/useCallback
-
不要为了“可能的优化”而滥用。
-
缓存本身也有开销(存储、比较依赖项)。
-
只在以下情况使用:
- 计算确实昂贵(如大数据处理);
- 导致子组件无谓重渲染(配合
memo); - 作为 props 传递给已优化的子组件。
❌ 误区2:依赖项遗漏
const handleClick = useCallback(() => {
console.log(count) // 依赖 count
}, []) // ❌ 错误!应该写 [count]
这会导致函数捕获旧的 count 值(闭包陷阱)。
✅ 正确做法:所有外部变量都必须出现在依赖数组中。
✅ 最佳实践
- 先写逻辑,再优化:不要过早优化。
- 配合 React DevTools Profiler:定位真实性能瓶颈。
- useMemo 用于值,useCallback 用于函数。
-
依赖项要完整且精确:使用 ESLint 插件
eslint-plugin-react-hooks自动检查。
六、总结:性能优化的哲学
useMemo 和 useCallback 并非银弹,而是 React 赋予我们的精细控制权。它们让我们能够:
- 隔离变化:让无关状态的更新不影响其他部分;
- 减少冗余:避免重复计算和渲染;
- 提升用户体验:使应用更流畅、响应更快。
正:
“count 和 keyword 不相关”
“某一个数据改变,只想让相关的子组件重新渲染”
这正是 React 性能优化的核心思想:局部更新,全局协调。
附:完整代码地址
源码地址:react/memo/memo/src/App.jsx · Zou/lesson_zp - 码云 - 开源中国
🎉 掌握
useMemo与useCallback,你已经迈入 React 性能优化的高手之列!
下次遇到“为什么子组件总在乱渲染?”或“计算太慢怎么办?”,你就知道答案了。
Happy coding! 🚀
Seedance 2.0之后,字节跳动又发布豆包大模型2.0
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-js 2025详细解读
往期文章:
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-js 2021 & state-of-css 2021详细解读
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-css 2022 & state-of-js 2022详细解读
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-css 2023详细解读
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-js 2023详细解读
【前端趋势调查系列】带你看看前端生态圈的技术趋势state-of-css 2024和state-of-js 2024详细解读
一、写在前面
- 本次分享的数据来源是state-of-js,是由Devgraphics开源社区团队发起的前端生态圈中规模最大的数据调查。
- 想要贡献state-of-js调查结果中文翻译的同学可以联系我,或者直接向Devographics/locale-zh-Hans这个仓库提PR,然后艾特我来帮你review。
- 如果这篇文章有其他意见或更好的建议,欢迎各位同学们多多指教。
二、受访者统计
今年的state-of-js调查共回收了13002份问卷结果。和去年相问卷结果又少了一些。
其实自从2022年起,填写问卷的人就越来越少,原因无外乎这么几个:
- 前端的整体热度都在走低,像是google trends上前端相关的搜索词的热度都在下降;
- 问卷内容过长导致内容填写起来比较麻烦;
- 受访者虽然一直关注这项调查,但填了第一年的问卷之后第二年的问卷就不填了等等。
而在今年我结合我在Datawhale做的一些数据调查来看,有一个更重要的原因,就是AI的崛起——大部分开发者们的注意力已经转向了AI领域(包括我自己也是),基本不会在前端领域投入过多关注了。
之前我也和调查发起人@SachaG聊过state-of-js调查的未来,作为一项坚持了9年的前端数据调查,也算是见证了前端领域的崛起与衰落。而如今,前端领域的热度早已不再是当年的样子,这项调查也不知道还能做多少年,大家且看且珍惜吧。
三、JS特性
语法特性
![]()
从今年的语法特性使用情况来看,社区对提升代码健壮性和简洁性的新特性抱有极大的热情:
-
空值合并 运算符 (
??) 的使用率高达 87% ,已经成为事实上的标准,这说明开发者在处理null或undefined时,迫切需要一种比||更严谨、更明确的工具来避免将0或false等有效值意外覆盖,在日常开发中,我们应当优先使用??来处理默认值赋值,以增强代码的稳定性和可预测性。 -
动态导入(
Dynamic Import) 以 66% 的使用率紧随其后,反映出代码分割和按需加载已是现代 Web 应用性能优化的核心实践,在构建大型应用、特别是需要考虑首屏加载速度的场景时,动态导入几乎是必修课。 -
类私有字段(
Private Fields) 和 逻辑赋值 运算符 (Logical Assignment) 的使用率分别为 43% 和 35% ,表明封装和代码简写同样是开发者追求的目标,尤其是私有字段,为在团队协作中保护内部状态、减少意外修改提供了语言层面的保障。
Array、Set、Object的特性
![]()
![]()
![]()
今年对 Array、Set、Object 数据结构的新特性调查,揭示了不可变性(Immutability) 和 数据处理便利性 已成为前端开发的核心趋势:
- 返回新数组的
toSorted()使用率已达 47% ,其孪生兄弟toReversed()也达到 37% ,说明社区正主动避免原地修改数组带来的副作用。 -
Set新方法整体普及度不高,但在使用者中union()、intersection()、difference()等集合运算需求最集中,开始用于表达更复杂的数据关系与权限逻辑。 - 首次进入调查的
Object.groupBy()拿到 39% 使用率,说明了“按字段分组”这类高频需求可以摆脱 Lodash 等库,直接靠原生 JS 优雅解决。
Promise的特性
![]()
在异步编程领域,对多个 Promise 的精细化控制能力已成为现代前端的标配:
-
Promise.allSettled()以 52% 的使用率登顶,适合在“批量请求但不希望单点失败拖垮整体流程”的场景下使用,例如并行拉取多个非关键数据源、日志或埋点结果,它能保证我们总能拿到每个 Promise 的最终状态。 -
Promise.any()使用率也达到 47% ,是“抢最快一个结果”的利器,典型场景是对多个镜像服务发起并行请求、谁先返回就用谁,从而显著优化响应延迟。 - 这两个 API 的走红说明前端异步模型已经从“能并发”走向“可编排”,开发者不再满足于简单的
Promise.all,而是开始为不同业务场景选择更合适的并发策略。
浏览器API
![]()
浏览器 API 的使用情况反映了 Web 应用能力正从传统的页面展示,向功能更丰富、更接近原生应用的形态演进:
-
WebSocket仍以 64% 的使用率牢牢占据基础设施地位,支撑了社交、协作、监控看板等场景中的实时通信。 - PWA 使用率达到 48% ,说明离线能力、安装体验和通知能力已经被越来越多团队纳入评估维度。
- 更值得关注的是
WebAssembly (WASM),使用率已达 21% 且排名上升 2 位,高性能语言(如 C++、Rust)编译到浏览器侧解决音视频处理、加解密、游戏等计算密集型问题,正在从先锋实践迈向工程常规武器。
JS语言的痛点
![]()
关于 JS 语言自身的痛点,今年的结果再次印证了社区共识:
- 缺乏静态类型(Static Typing) 以 28% 的提及率高居第一,这直接解释了为何 TypeScript 能在短时间内成为事实标准——大型项目在可维护性、重构安全和错误提前暴露上的诉求远非动态类型所能满足。
-
日期处理(Dates) 以 10% 排名第二,说明即便有
Temporal提案在推进,现实中开发者仍大量依赖date-fns、Day.js等第三方库来填补标准库短板。 - 同时,ESM 与 CJS 的兼容问题、标准库整体匮乏 等历史包袱也依然是工程实践中的绊脚石,这些痛点共同构成了“JS 好用但不够省心”的真实写照。
浏览器的痛点
![]()
当我们把视线从语言本身转向其运行环境——浏览器时,痛点显得更具工程现实感:
- 跨浏览器支持(Browser support) 以 31% 的提及率稳居首位,说明即便现代浏览器在标准实现上趋于一致,边缘行为差异、新特性落地节奏和兼容性策略仍是困扰前端团队的主要问题。
- 浏览器测试(Browser testing) 以 13% 位列第二,本质上是跨浏览器差异在测试和回归成本上的放大反馈
- 而被单独点名的 Safari 以 7% 成为第三大痛点,很多团队已经默认把它视作“新时代的 IE”,其标准跟进节奏和独特限制,为跨端一致性和平滑体验带来了额外负担。
四、JS技术
综述
![]()
![]()
这两张图分别从“历史趋势”和“当前满意度”两个维度,为我们描绘了 JS 技术生态的全景图:
- 左侧四象限清晰展示出以 Vite 为代表的新一代工具,正沿着“低使用、高满意度”向“高使用、高满意度”高速跃迁,而曾经的王者 webpack 虽然仍有庞大使用量,但满意度明显滑落且轨迹线转为紫色,显示出疲态
- 从右侧满意度分级我们可以发现,Vite (98%) 、Vitest (97%) 、Playwright (94%) 、Astro (94%) 等新星占据 S 级,而 webpack (26%) 、Angular (48%) 、Next.js (55%) 等传统选手则跌入 B/C 级,这意味着“存量巨大但口碑一般”的技术栈随时可能迎来用户流失;同时,Vite 生态中 Vite + Vitest 的双双登顶也说明高度协同的一体化工具链的优势,对于开发者而言,技术选型时不能只看当前占有率,更要关注满意度和趋势曲线,尤其要多留意那些位于右下象限、线条仍在上扬的新工具。
前端框架
![]()
前端框架的长期“三巨头”格局正在被悄然改写:
- React 依旧以 80%+ 的使用率牢牢占据生态核心,但满意度已滑落到 B 级(72%),复杂的心智模型和渐进式演化成本让不少团队收到困扰。
- Vue.js 在 2022 年前后正式超越 Angular 成为第二大框架,并以 84% 的满意度稳居 A 级,证明其在开发体验与性能之间找到了不错的平衡点。
- Svelte 则凭借“无虚拟 DOM”的编译时理念持续走高,使用率已升至 26% ,成为追求极致性能和简洁语法团队的心头好。
- 更有意思的是 HTMX,在近两年实现爆发式增长、使用率来到 13% ,它用“回归 HTML、用属性驱动交互”的思路,对当下 JS-heavy 的前端栈提出了有力反思。
元框架(前后端一体化框架)
![]()
元框架领域呈现出“一家独大 + 新星涌现”的混合格局:
- Next.js 继续凭借与 React 的深度绑定,以近 60% 的使用率统治榜单,是大多数 React 团队构建生产级应用的默认选项,App Router 等激进改动和整体复杂度的提升正在透支开发者耐心。
- Nuxt 在 Vue 生态中稳扎稳打,使用率升至 28% 。
- Astro 与 SvelteKit 则是近年最值得关注的两颗新星,前者在内容密集型站点中大放异彩,后者与 Svelte 深度绑定,为全栈应用提供了端到端的极致体验。
后端框架
![]()
在 Node.js 后端框架领域,我们不难看出,还是有些新面孔:
- 老牌选手 Express 仍以 80%+ 的使用率稳居第一,作为“薄核心 + 丰富中间件”的事实标准难以被完全替代,但 81% 的满意度也表明开发者正在寻找更现代的方案
- tRPC 是过去两年最耀眼的新星,通过直接在 TypeScript 中实现端到端类型安全调用,大幅简化了前后端联调与接口演进的成本。
测试框架
![]()
JavaScript 测试生态正在经历一场“现代化重构”:
- 在单元与集成测试层面,Jest 以 75% 的使用率独占鳌头。
- 端到端测试领域则被 Cypress (55%) 和 Playwright (49%) 两强主导,其中 Playwright 以 94% 的满意度跻身 S 级,体现了其在稳定性、调试体验和多浏览器支持上的优势。
- 紧随其后的是 Vitest,作为 Vite 生态的一员,在短短两年内使用率冲到 50% ,满意度更是高达 97% ,验证了“测试工具与构建工具深度一体化”带来的体验红利。
构建工具
![]()
前端构建工具领域也在发生变革:
- webpack 依旧以 85% 的使用率占据绝对存量,但满意度已经跌至 26% ,复杂配置和缓慢构建让它越来越像一座难以完全迁移的“基础设施债务”。
- Vite 则是新时代的领跑者,使用率在短短数年间拉升到 83% 、几乎追平 webpack,满意度更是高达 98% ,依托基于 Go 的 esbuild 实现极快冷启动和热更新,重新定义了“本地开发体验”的下限
- 在更底层 esbuild 的直接使用率已达 52% ,SWC 也拿到 83% 的满意度,说明社区正将编译热点下沉到 Rust/Go 等原生实现,再在其之上搭建更友好的工具。
五、其它工具
JS库使用情况
![]()
在通用 JS 库层面,数据清晰地表明开发者最在乎两件事:
- 类型安全和数据处理效率。以 TypeScript 为优先设计的校验库 Zod 以 48% 的使用率登顶,成为“运行时数据校验 + 类型推导”领域的绝对主角,反映出大家在 API 返回、表单输入等链路上,对类型与数据一致性的强烈诉求。
- 传统工具库 Lodash (39%) 依然宝刀不老,仍在大量项目中承担通用数据处理职责。
- 而在日期处理上,date-fns (39%) 、Moment (25%) 、Day.js (24%) 等多家共存,本质上是对 JS 原生日期能力长期缺位的弥补
- 即便是已经被视作“老古董”的 jQuery (16%) ,也仍凭借海量遗留项目保持着不可忽视的存在感。
AI使用情况
![]()
AI 工具已经深度嵌入前端开发者的日常工作流,成为新的基础设施:
- ChatGPT 以 60% 的使用率位居首位,承担了问答、代码草稿生成、调试思路辅助等“外脑”角色。
- 深度集成 IDE 的 GitHub Copilot 使用率也达 51% ,更偏向于在写代码时提供上下文感知补全与重构建议,两者形成“离线思考 + 在线自动补全”的互补关系
- 与此同时,Claude (44%) 、Google Gemini (28%) 等通用大模型产品也在快速补位,说明开发者愿意多源头对比体验
- 值得注意的是 AI-native 编辑器 Cursor 已有 26% 的使用率,一部分人开始直接迁移到“以 AI 为核心交互对象”的编辑环境中,这预示着未来开发工具形态本身也会被 AI 重塑。
- 另外,国产大模型 Deepseek 也榜上有名,占据了 8% 的使用率。
其它编程语言使用情况
![]()
这张图展示了 JS 开发者的多语言画像:
- Python 以 41% 的占比成为最常见的第二语言,依托后端开发、自动化脚本、数据分析与 AI 等丰富场景,为前端同学打开了更多技术边界。
- PHP (27%) 的存在感说明不少人仍在使用 Web 传统栈构建项目或是在维护古老的历史项目。
- 在工具链和 DevOps 侧,Bash (22%) 几乎是所有工程师的“必修课”。
- 而 Java (21%) 、Go (20%) 、C# (19%) 等企业级后端语言,以及以安全与性能著称的 Rust (16%) ,则构成了很多前端开发者向全栈或更底层系统方向延展的技能支点。
六、使用情况及痛点问题
TS与JS的使用情况
![]()
这张分布图有力地说明,TypeScript 已经从“可选增强”进化为 JavaScript 生态的默认选项:
- 有 48% 的受访者表示项目代码 100% 使用 TS 编写,体现出“一旦采用就倾向于全量迁移”的强烈偏好;在所有项目(包括纯 JS、纯 TS 与混合工程)中计算得到的平均采用率高达 77% ,意味着当今前端代码大部分都运行在类型系统保护之下;仍坚持纯 JS 的开发者仅占 6% ,多半集中在遗留项目或极轻量脚本场景;对于在做技术选型的新项目来说,这几乎已经构成了一个共识结论:默认使用 TS,而不是再纠结要不要上 TS。
AI代码生成情况
![]()
这张图刻画了 AI 在代码生成中的“真实渗透率”,结论很清晰:
- AI 目前更像是开发者的“副驾驶”,而非自动写代码的主力工程师。只有 10% 的受访者认为项目代码完全没有 AI 贡献,说明九成以上的团队或多或少已经在用 AI 提效;最集中的区间是 1%–20% 代码由 AI 生成(占 38% ),典型用法是让 AI 帮忙写模板代码、样板逻辑、特定算法实现或提供重构建议,而不是让它从零实现完整模块;总体算下来,平均约有 29% 的代码可以归功于 AI,这是一个不容忽视但远未到“全自动开发”的比例,也意味着复杂业务建模、架构设计和质量把控这些高阶工作,短期内仍牢牢掌握在人类开发者手中。
JS的痛点问题
![]()
在所有 JS 开发痛点中,真正让团队头疼的并不是某个语法细节,而是宏观层面的工程复杂度:
- 代码架构(Code Architecture) 以 38% 的提及率高居榜首,说明随着前端项目体量和生命周期不断拉长,如何拆分模块、划分边界、治理依赖、避免“屎山”成为最大挑战。
- 紧随其后的是 状态管理(State Management,34%) ,无论是 React 的 hooks 与各种状态库,还是 Vue 的 Pinia,跨组件、跨页面的复杂状态流转依然极易失控。
-
依赖管理(Managing Dependencies,32%) 也是老大难问题,
node_modules黑洞、版本冲突、安全漏洞以及 ESM/CJS 兼容性都会侵蚀工程稳定性。 - 相对而言,曾经广受诟病的 异步 代码(Async Code) 如今只剩 11% 的人视其为痛点,
Promise与async/await已经在很大程度上平滑了这块心智负担,这也从侧面证明语言与工具的演进确实可以逐步“消灭”一部分历史问题。
七、总结
首先,毫无疑问,TypeScript 已然胜出。它赢下的不只是「能编译成js的工具」的争论,而是语言本身。Deno 和 Bun 早已原生支持它。如今,你甚至能在稳定版 Node.js 中直接编写 TypeScript了。
而 Vite 的时代也已到来。今年,Vite 的下载量正式超越 webpack。与之相伴,Vitest 的使用量也大幅飙升。现在正是切换到新一代 Vite 工具链的好时机,而 2026 年注定会是全面落地之年—— 随着 Rolldown 稳定版发布,将驱动出更快的新一代 Vite,同时还有一体化的「Vite+」值得期待。
我们的开发工具从未如此优秀。但大家如今真正关心的却是另一个问题:AI 又将带来什么?
AI 即将彻底改变我们查阅文档、编写代码、做架构决策等一系列工作方式。各家公司都在全力押注全新的开发模式。对我们绝大多数人而言,AI 编程助手正在改变我们与代码交互的方式。
这是一件好事吗?
截至 2025 年底,已有近 30% 的代码由 AI 生成。Cursor 的人气暴涨,尽管它们暂时还无法撼动 VS Code 第一 IDE 的地位。而基于智能代理的工具,比如 Claude、Gemini 和 Copilot,也在迅速普及。
对开发者来说,无论使用什么工具,懂得分辨「什么是好代码」 将会比以往任何时候都更重要。紧跟新语言特性、知道该基于哪些库去开发,而非凭感觉从零手写一切,也会变得愈发关键。
现在,一天之内快速搭建新项目、轻松迁移老项目都已成为现实。这对框架和库的作者来说是个挑战。我们必须保证工具能持续服务好开发者,不能指望用户会一直因惯性而使用。
而这一点,恰恰值得所有开发者的期待。
就让我们拭目以待 2026 年的变化吧。我期待着更快的工具、更好的开发体验,以及技术真正成为能力放大器,强化我们自身的判断与选择。
父传子全解析:从基础到实战,新手也能零踩坑
在 Vue3 组件化开发中,父传子是最基础、最常用的组件通信方式,也是新手入门组件通信的第一步。无论是传递简单的字符串、数字,还是复杂的对象、数组,甚至是方法,父传子都有清晰、规范的实现方式。
不同于 Vue2 选项式 API 中 props 的写法,Vue3 组合式 API(
一、核心原理:单向数据流 + Props 传值
Vue3 父传子的核心逻辑只有两个关键词:Props 和 单向数据流。
- Props:父组件通过在子组件标签上绑定属性(类似 HTML 标签属性),将数据传递给子组件;子组件通过定义 props,接收父组件传递过来的数据,相当于子组件的「输入参数」。
- 单向数据流:数据只能从父组件流向子组件,子组件不能直接修改父组件传递过来的 props 数据(否则会报错)。如果子组件需要修改 props 数据,必须通过子传父的方式,通知父组件修改原始数据。
记住一句话:Props 是只读的,修改需找父组件。这是 Vue 组件通信的核心规范,也是避免数据混乱的关键。
父传子的核心流程(3步走):
- 父组件:在使用子组件的标签上,通过
:属性名="要传递的数据"绑定数据; - 子组件:通过
defineProps定义要接收的 props(声明属性名和类型,可选但推荐); - 子组件:在模板或脚本中,直接使用 props 中的数据(无需额外导入,直接通过 props.属性名 或 直接写属性名使用)。
二、基础用法:最简洁的父传子实现(必学)
我们用一个「父组件传递基本数据,子组件展示」的简单案例,讲解最基础的父传子写法,代码可直接复制到项目中运行,零门槛上手。
1. 父组件(Parent.vue):绑定数据并传递
<template>
<div class="parent">
<h3>我是父组件</h3>
<p>父组件的基本数据:{{ parentName }}、{{ parentAge }}</p>
<p>父组件的数组:{{ parentList.join('、') }}</p>
<p>父组件的对象:{{ parentObj.name }} - {{ parentObj.gender }}</p>
<!-- 1. 核心:在子组件标签上,通过 :属性名 绑定要传递的数据 -->
<Child
:name="parentName" // 传递字符串
:age="parentAge" // 传递数字
:list="parentList" // 传递数组
:user-info="parentObj" // 传递对象(推荐用短横线命名)
/>
</div>
</template>
<script setup>
// 引入子组件(Vue3 <script setup> 中,引入后可直接在模板中使用)
import Child from './Child.vue'
import { ref, reactive } from 'vue'
// 父组件要传递的数据(涵盖基本类型、数组、对象)
const parentName = ref('张三') // 字符串
const parentAge = ref(25) // 数字
const parentList = ref(['苹果', '香蕉', '橙子']) // 数组
const parentObj = reactive({ // 对象
name: '李四',
gender: '男',
age: 30
})
</script>
2. 子组件(Child.vue):定义Props并使用
<template>
<div class="child">
<h4>我是子组件(接收父组件传递的数据)</h4>
<p>接收的字符串:{{ name }}</p>
<p>接收的数字:{{ age }} 岁</p>
<p>接收的数组:{{ list.join('、') }}</p>
<p>接收的对象:{{ userInfo.name }}({{ userInfo.gender }})</p>
</div>
</template>
<script setup>
// 2. 核心:通过 defineProps 定义要接收的 props
// 写法1:数组形式(简单场景,只声明属性名,不限制类型)
// const props = defineProps(['name', 'age', 'list', 'userInfo'])
// 写法2:对象形式(推荐,可限制类型、设置默认值、必填校验)
const props = defineProps({
// 字符串类型
name: {
type: String,
default: '默认用户名' // 默认值(父组件未传递时使用)
},
// 数字类型
age: {
type: Number,
default: 18
},
// 数组类型(注意:数组/对象的默认值必须用函数返回,避免复用污染)
list: {
type: Array,
default: () => [] // 数组默认值:返回空数组
},
// 对象类型(同理,默认值用函数返回)
userInfo: {
type: Object,
default: () => ({}) // 对象默认值:返回空对象
}
})
// 3. 在脚本中使用 props 数据(通过 props.属性名)
console.log('脚本中使用props:', props.name, props.age)
</script>
3. 基础细节说明(新手必看)
-
defineProps是 Vue3 内置宏,无需导入,可直接在 - 父组件传递数据时,属性名推荐用
kebab-case(短横线命名),比如:user-info,子组件接收时用camelCase(小驼峰命名),比如userInfo,Vue 会自动做转换; - 数组/对象类型的 props,默认值必须用
函数返回(比如default: () => []),否则多个子组件会复用同一个默认值,导致数据污染; - 子组件模板中可直接使用 props 的属性名(比如
{{ name }}),脚本中必须通过props.属性名使用(比如props.name)。
三、进阶用法:优化父传子的体验(实战常用)
基础用法能满足简单场景,但在实际开发中,我们还会遇到「必填校验」「类型多可选」「props 数据转换」等需求,这部分进阶技巧能让你的代码更规范、更健壮,避免后续维护踩坑。
1. Props 校验:必填项 + 多类型 + 自定义校验
通过 defineProps 的对象形式,我们可以对 props 进行全方位校验,避免父组件传递错误类型、遗漏必填数据,提升代码可靠性。
<script setup>
const props = defineProps({
// 1. 必填项校验(required: true)
username: {
type: String,
required: true, // 父组件必须传递该属性,否则控制台报警告
default: '' // 注意:required: true 时,default 无效,可省略
},
// 2. 多类型校验(type 为数组)
id: {
type: [Number, String], // 允许父组件传递数字或字符串类型
default: 0
},
// 3. 自定义校验(validator 函数)
score: {
type: Number,
default: 0,
// 自定义校验规则:分数必须在 0-100 之间
validator: (value) => {
return value >= 0 && value <= 100
}
}
})
</script>
说明:校验失败时,Vue 会在控制台打印警告(不影响代码运行),但能帮助我们快速定位问题,尤其适合团队协作场景。
2. Props 数据转换:computed 处理 props 数据
子组件不能直接修改 props 数据,但可以通过 computed 对 props 数据进行转换、格式化,满足子组件的展示需求,不影响原始 props 数据。
<template>
<div class="child">
<p>父组件传递的分数:{{ score }}</p>
<p>转换后的等级:{{ scoreLevel }}</p>
<p>父组件传递的姓名(大写):{{ upperName }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
score: {
type: Number,
default: 0
},
name: {
type: String,
default: ''
}
})
// 对 props 分数进行转换:0-60 不及格,60-80 及格,80-100 优秀
const scoreLevel = computed(() => {
const { score } = props
if (score >= 80) return '优秀'
if (score >= 60) return '及格'
return '不及格'
})
// 对 props 姓名进行格式化:转为大写
const upperName = computed(() => {
return props.name.toUpperCase()
})
</script>
3. 传递方法:父组件给子组件传递回调函数
父传子不仅能传递数据,还能传递方法(回调函数)。核心用途:子组件通过调用父组件传递的方法,通知父组件修改数据(解决子组件不能直接修改 props 的问题)。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<p>父组件计数器:{{ count }}</p>
<!-- 传递方法::方法名="父组件方法" -->
<Child
:count="count"
:addCount="handleAddCount" // 传递父组件的方法
/>
</div>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const count = ref(0)
// 父组件的方法(将被传递给子组件)
const handleAddCount = () => {
count.value++
}
</script>
<!-- 子组件(Child.vue) -->
<template>
<div class="child">
<p>子组件接收的计数器:{{ count }}</p>
<!-- 调用父组件传递的方法 -->
<button @click="addCount">点击让父组件计数器+1</button>
</div>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
default: 0
},
// 声明接收父组件传递的方法(type 为 Function)
addCount: {
type: Function,
required: true
}
})
// 也可以在脚本中调用父组件的方法
const callParentMethod = () => {
props.addCount()
}
</script>
注意:传递方法时,父组件只需写 :addCount="handleAddCount"(不带括号),子组件调用时再带括号 addCount();如果父组件写 :addCount="handleAddCount()",会导致方法立即执行,而非传递方法本身。
4. 批量传递 props:v-bind 绑定对象
如果父组件需要给子组件传递多个 props,逐个绑定会比较繁琐,这时可以用 v-bind 批量绑定一个对象,子组件只需对应接收即可。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 批量传递:v-bind="对象",等价于逐个绑定对象的属性 -->
<Child v-bind="userObj" />
</div>
</template>
<script setup>
import Child from './Child.vue'
import { reactive } from 'vue'
// 要批量传递的对象
const userObj = reactive({
name: '张三',
age: 25,
gender: '男',
address: '北京'
})
</script>
<!-- 子组件(Child.vue) -->
<script setup>
// 逐个接收父组件批量传递的 props,和普通 props 接收一致
const props = defineProps({
name: String,
age: Number,
gender: String,
address: String
})
</script>
四、实战场景:父传子的高频应用(贴合实际开发)
结合实际开发中的高频场景,补充 3 个常用案例,覆盖大部分父传子需求,直接套用即可。
场景1:父组件控制子组件弹窗显示/隐藏
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<button @click="visible = true">打开子组件弹窗</button>
<!-- 传递弹窗显示状态 + 关闭弹窗的方法 -->
<ChildModal
:visible="visible"
:closeModal="handleCloseModal"
/>
</div>
</template>
<script setup>
import ChildModal from './ChildModal.vue'
import { ref } from 'vue'
const visible = ref(false)
// 关闭弹窗的方法
const handleCloseModal = () => {
visible.value = false
}
</script>
<!-- 子组件(ChildModal.vue) -->
<template>
<div class="modal" v-if="visible">
<div class="modal-content">
<h4>子组件弹窗</h4>
<button @click="closeModal">关闭弹窗</button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
closeModal: {
type: Function,
required: true
}
})
</script>
场景2:父组件给子组件传递接口数据
实际开发中,父组件通常会请求接口,将接口返回的数据传递给子组件展示,这是最常见的场景之一。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 加载中状态 -->
<div v-if="loading">加载中...</div>
<!-- 接口数据请求成功后,传递给子组件 -->
<ChildList :list="goodsList" v-else />
</div>
</template>
<script setup>
import ChildList from './ChildList.vue'
import { ref, onMounted } from 'vue'
const goodsList = ref([])
const loading = ref(false)
// 父组件请求接口
onMounted(async () => {
loading.value = true
try {
const res = await fetch('https://api.example.com/goods')
const data = await res.json()
goodsList.value = data.list // 接口返回的列表数据
} catch (err) {
console.error('接口请求失败:', err)
} finally {
loading.value = false
}
})
</script>
场景3:子组件复用,父组件传递不同配置
子组件复用是组件化开发的核心优势,通过父传子传递不同的配置,让同一个子组件实现不同的展示效果。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 同一个子组件,传递不同配置,展示不同效果 -->
<Button
:text="按钮1"
:type="primary"
:disabled="false"
/>
<Button
:text="按钮2"
:type="default"
:disabled="true"
/>
</div>
</template>
<script setup>
import Button from './Button.vue'
</script>
<!-- 子组件(Button.vue) -->
<template>
<button
class="custom-btn"
:class="type === 'primary' ? 'btn-primary' : 'btn-default'"
:disabled="disabled"
>
{{ text }}
</button>
</template>
<script setup>
const props = defineProps({
text: {
type: String,
required: true
},
type: {
type: String,
default: 'default',
validator: (val) => {
return ['primary', 'default', 'danger'].includes(val)
}
},
disabled: {
type: Boolean,
default: false
}
})
</script>
五、常见坑点避坑指南(新手必看)
很多新手在写父传子时,会遇到「props 接收不到数据」「修改 props 报错」「方法传递后无法调用」等问题,以下是最常见的 5 个坑点,帮你快速避坑。
坑点1:父组件传递数据时,忘记加冒号(:)
错误写法:<Child name="parentName"></Child>(没有冒号,传递的是字符串 "parentName",而非父组件的 parentName 变量);
正确写法:<Child :name="parentName"></Child>(加冒号,传递的是父组件的变量)。
坑点2:子组件直接修改 props 数据
错误写法:props.name = '李四'(直接修改 props,会报错);
正确写法:通过父传子的方法,通知父组件修改原始数据(参考「传递方法」章节),或通过 computed 转换数据(不修改原始 props)。
坑点3:数组/对象 props 的默认值未用函数返回
错误写法:list: { type: Array, default: [] }(直接写数组,会导致多个子组件复用同一个数组,数据污染);
正确写法:list: { type: Array, default: () => [] }(用函数返回数组,每个子组件都会得到一个新的空数组)。
坑点4:传递方法时,父组件带了括号
错误写法:<Child :addCount="handleAddCount()"></Child>(方法立即执行,传递的是方法的返回值,而非方法本身);
正确写法:<Child :addCount="handleAddCount"></Child>(不带括号,传递方法本身)。
坑点5:props 命名大小写不一致
错误写法:父组件 :userInfo="parentObj",子组件接收 userinfo(小写 i);
正确写法:父组件用 kebab-case(:user-info),子组件用 camelCase(userInfo),或保持大小写一致(不推荐)。
六、总结:父传子核心要点回顾
Vue3 父传子的核心就是「Props 传值 + 单向数据流」,记住以下 4 个核心要点,就能应对所有父传子场景:
- 基础流程:父组件
:属性名="数据"绑定 → 子组件defineProps接收 → 子组件使用数据; - 核心规范:Props 是只读的,子组件不能直接修改,修改需通过父传子的方法通知父组件;
- 进阶技巧:props 校验提升可靠性,computed 转换数据,v-bind 批量传值,传递方法实现双向交互;
- 避坑关键:加冒号传递变量、不直接修改 props、数组/对象默认值用函数返回、传递方法不带括号。
父传子是 Vue3 组件通信中最基础、最常用的方式,掌握它之后,再学习子传父、跨层级通信(provide/inject)、全局通信(Pinia)会更轻松。
成都天府大道车辆碰撞事故完成责任认定,酒后驾车当事人全责
摩根士丹利邢自强:建议试点贴按揭100基点 缩小与租金回报率差距
真我 Neo8 体验:从性能、显示到影像来了一次全面升级,真正全面的性价比旗舰
![]()
最近,真我发布了 Neo 系列新机——真我 Neo8,首销价 2399 元,国补到手价 2039.15 元。
同样定位在 2000 元档的性能旗舰上,真我这次可算是给 Neo 系列来了一次非常全面的升级。不仅升级了骁龙芯片和更大的电池,屏幕性能再上了一个档次,还加上了潜望式长焦镜头,从主打性能的机型变成全方位提升的产品。
![]()
先讲最重要的性能,真我 Neo8 这次搭载最新的第五代高通骁龙 8 移动平台,搭配 LPDDR5X RAM 和 UFS 4.1 ROM 的储存组合。机身内置了大气流冷锋散热系统,总散热面积为 39225mm²,覆盖了 65% 的机身面积,以提升散热效能。
为了提升手机的性能释放能力,真我这次在 Neo8 的游戏空间中加入了新的极客性能面板,用户可以在自由调节 CPU、GPU 频率,还有内置五档温度调节,尽可能提升游戏性能。
![]()
Neo8 也搭载了新一代 GT 性能引擎,透过「先知能效调度技术」进行更精准的性能,提升重负载状态下的稳帧表现。
目前,真我 Neo8 支持《三角洲行动》、《暗区突围》、《和平精英》、《使命召唤手游》、《穿越火线手游》等游戏的原生 165 帧模式,《无畏契约》和《王者荣耀》的原生 144 帧模式,《原神》和《崩坏:星穹铁道》的 120 + 1.5k 超分超帧模式。
![]()
性能释放升级之后,真我在 Neo8 上加入了 PC 掌机模式。
这个模式可以绑定 Steam 账号并下载里面的内容,以及游戏存档同步的功能,目前《空洞骑士:丝之歌》、《哈迪斯 2》、《只狼:影逝二度》、《古墓丽影 9》、《女神异闻录 4 黄金版》等游戏。
![]()
屏幕部分,Neo8 用上了一块 6.78 英寸165Hz 三星苍穹屏。
屏幕搭载了三星 M14 发光材质,手动最高亮度为 1000nits,全局峰值亮度达到 18nits,局部峰值亮度可达到 6500nits,还有个 20% 窗口下支持 3800nits 显示的阳光模式。屏幕还支持「全亮度 DC 调光+硬件级低蓝光+智能护眼」组合,有 TUV 莱茵无频闪认证和可以智能调节显示参数,保证护眼的前提下提升显示准度。
![]()
屏幕显示刷新率最高位 165Hz,支持真我 GT8 Pro 同款的游戏触显同步技术,瞬时触控最高刀到 3800Hz,十字触控有 360Hz 报点率。
最直观的体验就是在 FPS 的时候操作可以更加跟手,开镜响应更快,显示的卡顿会在少一点。
![]()
续航方面,真我这次给到一块 8000mAh 的电池,容量比 GT8 标准版还要大 1000mAh。日常使用,只要不是经常用最高性能的模式打游戏,那坚持两天还是没有问题的。
比起续航能力,Neo8 充电性能的变化会来得更有吸引力。
手机支持 80W 闪充,并支持 UFCS、PPS、PD、QC 的全协议栈快充和旁路供电,8000mAh 的手机用自家快充组合需要 75 分钟,接入 AI 小电拼 Ultra 最高支持 51W 充电,0-100 充电需要 77 分钟。这个成绩看来,Neo8 是真的可以彻底告别官方充电器限制的一款产品。
![]()
影像部分,真我 Neo8 选择的是同价位非常罕见的「超广+广角主摄+潜望式长焦」标准三摄。
主摄用了索尼 1/1.56 英寸 5000 万像素传感器,原生焦段支持 8K 超清直出。长焦部分用上了 3.5 倍光学变焦的潜望式镜头,支持 7 倍无损变焦以及最高 40 倍的数码变焦。相机内有 AI 望远算法的加持,进一步听声远摄的清晰度。
![]()
此外,Neo8 内置了影调模式,用户可以直接在相机内滑动选取不同的色彩质感,也可以自定义色彩、锐度、颗粒等参数。人像模式支持了 1x、1.5x、2x、3.5x 和 4x 的选项,机内还提供了 Live Photo 慢动作,丰富相机玩法。
不过,放到这个价位的手机身上,能够实现超广、主摄、长焦的整齐覆盖,而且长焦用的还是潜望式长焦,这一点就已经比很多定位性价比的机型要好了。
![]()
最后来看看外观,真我 Neo8 用了透明玻璃后盖设计,后盖加入了新一代漫反射工艺,手机后盖受光的情况下可以见到不同的光影效果。
透明后盖下有通过单层透明立体分区工艺,透过立体分区视觉做出 11 种纹理深度差低到 1.2μm 的差异化纹理和微球面处理,增加了后侧细节的丰富度。
![]()
换成透明后盖之后,真我经典的觉醒光环设计回归到 Neo8 DECO 右侧,环绕着 NFC 模块,可以根据提醒、游戏状态、开机等场景做光效切换。
![]()
机身采用了磨砂金属中框,提升了整体的手感和外观质感。
![]()
最后看看售价,真我 Neo8 有五个储存版本,首销价 2399 元起:
- 12GB + 256GB:首销价 2399 元,国补到手价 2039.15 元
- 16GB + 256GB:首销价 2699 元,国补到手价 2294.15 元
- 12GB + 512GB:首销价 2899 元,国补到手价 2464.15 元
- 16GB + 512GB:首销价 3199 元,国补到手价 2719.15 元
- 16GB + 1TB:首销价 3699 元,国补到手价 3199 元
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
去年国内风电新增吊装容量创历史新高,海外出口表现亮眼
春节假期期间 横琴口岸日均客流预计10万人次
京东回应“巴黎仓库遭遇盗窃”:绝大多数被盗物资被顺利追回
今天全国铁路预计发送旅客1535万人次
i18n Ally Next:重新定义 VS Code 国际化开发体验
![]()
做国际化(i18n)是前端开发中最「看起来简单、做起来要命」的事情之一。翻译文件散落各处,键名拼写错误要到运行时才发现,新增语言要手动逐个补全,源语言改了但翻译没跟上……如果你也深有同感,这篇文章就是写给你的。
为什么要做 i18n Ally Next?
i18n Ally 是 VS Code 生态中最受欢迎的国际化插件之一,由 @antfu 创建。它提供了内联注解、翻译管理、文案提取等一系列强大功能,极大地提升了 i18n 开发体验。
但随着时间推移,原项目的维护逐渐放缓。社区积累了大量 Issue 和 PR 未被处理,一些现代框架(如 Next-intl、Svelte 5)的支持也迟迟没有跟上。更关键的是——原项目没有文档站点,所有使用说明散落在 README 和 Wiki 中,新用户上手成本很高。
i18n Ally Next 正是在这样的背景下诞生的——我们 fork 了原项目,在保留所有经典功能的基础上,持续修复 Bug、新增特性、改善性能,并且从零搭建了完整的文档体系。
为什么文档这么重要?
原版 i18n Ally 功能强大,但有一个致命问题:你不知道它能做什么。
很多开发者安装后只用到了内联注解这一个功能,对审阅系统、自定义框架、正则匹配、路径匹配等高级功能一无所知。这不是用户的问题,是文档缺失的问题。
i18n Ally Next 的文档站点从以下几个维度重新组织了内容:
🧱 结构化的指南体系
- 快速开始 — 从安装到看到第一个内联注解,5 分钟搞定
- 支持的框架 — 完整列出 25+ 支持的框架及其自动检测机制
- 语言文件格式 — JSON、YAML、JSON5、PO、Properties、FTL……每种格式的用法和注意事项
- 命名空间 — 大型项目必备的翻译文件组织方式
- 文案提取 — 从硬编码字符串到 i18n 键的完整工作流
- 审阅系统 — 团队协作翻译的质量保障
- 机器翻译 — 8 种翻译引擎的配置与对比
🏗️ 框架最佳实践
不同框架的 i18n 方案差异很大。我们为每个主流框架编写了专属的最佳实践指南:
-
Vue I18n — SFC
<i18n>块、Composition API、Nuxt I18n - React & Next.js — react-i18next、Next-intl、Next-international
- Angular — ngx-translate、Transloco
- Svelte、Laravel 与 Rails — 各有专属配置示例
- 自定义框架 — 从零配置到完整运行的实战教程
📋 完整的配置参考
每一个配置项都有:类型、默认值、使用场景说明和代码示例。不再需要猜测某个配置是干什么的。
- settings.json 配置 — 100+ 配置项的完整参考
- 自定义框架配置 — YAML 配置文件的每个字段详解
新增功能详解
🤖 Editor LLM:零配置 AI 翻译
这是 i18n Ally Next 最具创新性的功能之一。它直接调用你编辑器内置的大语言模型进行翻译——无需 API Key,无需额外配置。
{ "i18n-ally-next.translate.engines": ["editor-llm"] }
它会自动检测你的编辑器环境:
- VS Code — 调用 GitHub Copilot
- Cursor — 调用 Cursor 内置模型
- Windsurf — 调用 Windsurf 内置模型
更强大的是,Editor LLM 支持批量翻译。当你选择翻译多个键时,它会将同一语言对的翻译请求合并为一次 API 调用,按 JSON 格式批量处理,大幅提升翻译速度并降低 token 消耗。
你也可以指定模型:
{ "i18n-ally-next.translate.editor-llm.model": "gpt-4o" }
🦙 Ollama:完全离线的本地翻译
对于有数据安全要求的团队,Ollama 引擎让你可以使用本地部署的大模型进行翻译,数据完全不出本机。
{
"i18n-ally-next.translate.engines": ["ollama"],
"i18n-ally-next.translate.ollama.apiRoot": "http://localhost:11434",
"i18n-ally-next.translate.ollama.model": "qwen2.5:latest"
}
通过 OpenAI 兼容 API 调用,支持任何 Ollama 上可用的模型。翻译 prompt 经过专门优化,能正确保留 {0}、{name}、{{variable}} 等占位符。
🔌 8 种翻译引擎全覆盖
| 引擎 | 特点 | 适用场景 |
|---|---|---|
| 免费、语言覆盖广 | 日常开发 | |
| Google CN | 国内可直接访问 | 国内开发者 |
| DeepL | 翻译质量最佳 | 面向用户的正式翻译 |
| OpenAI | 灵活、可自定义 API 地址 | 需要高质量 + 自定义 |
| Ollama | 完全离线、数据安全 | 企业内网环境 |
| Editor LLM | 零配置、批量翻译 | 快速迭代 |
| 百度翻译 | 国内 API、中文优化 | 中文项目 |
| LibreTranslate | 开源自托管 | 完全自主可控 |
引擎可以配置多个作为 fallback:
{ "i18n-ally-next.translate.engines": ["editor-llm", "deepl", "google"] }
🕵️ 陈旧翻译检测
这是一个容易被忽视但极其重要的功能。当源语言的文案发生变更时,其他语言的翻译可能已经过时了——但你不会收到任何提醒。
i18n Ally Next 的陈旧翻译检测(Stale Translation Check)解决了这个问题:
- 插件会记录每个键的源语言快照
- 当你运行检测命令时,它会对比快照与当前值
- 发现变更后,你可以选择:
- 全部重新翻译 — 一键将所有过期翻译发送到翻译引擎
- 逐个确认 — 逐条查看变更内容,决定是否重新翻译
- 仅更新快照 — 确认当前翻译仍然有效,更新基准
这意味着你的翻译永远不会「悄悄过期」。
🔍 全项目扫描与批量提取
单文件的硬编码检测很有用,但真正的 i18n 迁移需要全项目级别的能力。
「扫描并提取全部」命令可以:
- 扫描项目中所有支持的文件(可通过 glob 配置范围)
- 检测每个文件中的硬编码字符串
- 显示扫描结果摘要(N 个文件,M 个硬编码字符串)
- 确认后自动批量提取,为每个字符串生成键名并写入 locale 文件
{
"i18n-ally-next.extract.scanningInclude": ["src/**/*.{ts,tsx,vue}"],
"i18n-ally-next.extract.scanningIgnore": ["src/generated/**"],
"i18n-ally-next.extract.keygenStrategy": "slug",
"i18n-ally-next.extract.keygenStyle": "camelCase"
}
📝 审阅系统(v2的功能)
翻译不是一个人的事。i18n Ally Next 内置了审阅系统(Review System),支持:
- 逐条审阅翻译结果,标记为「通过」或「需修改」
- 留下评论,与团队成员异步协作
- 审阅数据以 JSON 存储在项目中,可纳入版本控制
- 翻译结果可先保存为候选翻译(
translate.saveAsCandidates),审阅通过后再正式写入
这意味着翻译质量不再是黑盒——每一条翻译都有迹可循。
自定义框架:支持任意 i18n 方案
这是 i18n Ally Next 最灵活的功能之一。无论你使用什么 i18n 库,甚至是团队自研的方案,都可以通过一个 YAML 配置文件让插件完美支持。
为什么需要自定义框架?
内置框架覆盖了 25+ 主流方案,但现实中总有例外:
- 公司内部封装的 i18n 工具函数
- 使用了非标准的翻译函数名(如
__(),lang(),msg()) - 新兴框架尚未被内置支持
- 需要同时匹配多种调用模式
如何配置?
在项目根目录创建 .vscode/i18n-ally-next-custom-framework.yml:
# 指定生效的文件类型
languageIds:
- typescript
- typescriptreact
- vue
# 正则匹配翻译函数调用,{key} 是占位符
usageMatchRegex:
- "\\Wt\\(\\s*['\"`]({key})['\"`]"
- "\\W\\$t\\(\\s*['\"`]({key})['\"`]"
- "\\Wi18n\\.t\\(\\s*['\"`]({key})['\"`]"
# 提取重构模板,$1 代表键名
refactorTemplates:
- "t('$1')"
- "{t('$1')}"
# 命名空间支持
namespace: true
namespaceDelimiter: ":"
# 作用域范围检测(如 React useTranslation hook)
scopeRangeRegex: "useTranslation\\(['\"](.+?)['\"]\\)"
# 是否禁用所有内置框架
monopoly: false
作用域范围是什么?
scopeRangeRegex 是一个非常实用的功能。以 React 为例:
const { t } = useTranslation("settings")
t("title") // → 自动解析为 "settings.title"
t("theme.dark") // → 自动解析为 "settings.theme.dark"
插件会根据正则匹配的结果自动划分作用域范围——从匹配位置到下一个匹配位置(或文件末尾)。在作用域内的所有 t() 调用都会自动加上命名空间前缀。
热重载
修改 YAML 配置文件后无需重启 VS Code,插件会自动检测文件变更并重新加载。这让调试正则表达式变得非常方便——改完立刻看效果。
快速上手
安装
在 VS Code 扩展面板搜索 i18n Ally Next,或从以下渠道安装:
最小配置
对于大多数项目,你只需要两步:
1. 指定语言文件路径
// .vscode/settings.json
{
"i18n-ally-next.localesPaths": ["src/locales"],
"i18n-ally-next.sourceLanguage": "zh-CN"
}
2. 打开项目,开始使用
插件会自动检测你的 i18n 框架(Vue I18n、react-i18next 等),无需额外配置。
打开任意包含翻译键的文件,你会看到:
- 🏷️ 翻译键旁边出现内联注解,直接显示翻译值
- 🌐 悬停键名可查看所有语言的翻译
- ✏️ 点击即可编辑翻译
- 📊 侧边栏显示翻译进度和缺失项
从 i18n Ally 迁移
如果你正在使用原版 i18n Ally,迁移非常简单:
- 卸载
i18n Ally - 安装
i18n Ally Next - 将
settings.json中的i18n-ally.前缀替换为i18n-ally-next.
所有配置项保持兼容,无需其他改动。
写在最后
国际化不应该是痛苦的。i18n Ally Next 的目标是让 i18n 成为开发流程中自然而然的一部分——写代码时看到翻译,提交前检查缺失,源文案变更时自动提醒,协作时有据可查。
我们不只是在做一个插件,更是在构建一套完整的 i18n 开发工具链:从文档到配置,从检测到提取,从翻译到审阅,每一个环节都有对应的解决方案。
如果你觉得这个项目有用,欢迎:
本文同步发布于 i18n Ally Next 官方文档。