news 2026/9/17 4:00:11

requestAnimationFrame:前端动画与JS性能优化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
requestAnimationFrame:前端动画与JS性能优化实战

做前端动画这些年,requestAnimationFrame(下面我简称 rAF)大概是那种“人人都听过、但真正吃透的人不多”的 API。它看着简单,一行requestAnimationFrame(fn)就能跑,可真要拿它来做 js 性能优化,里面的门道一点都不少。这篇文章我想聊的不是教科书式的定义,而是把这几年在项目里踩过的坑、对比过的数据、封装的工具函数都摊开来讲。不管你是刚学前端、只会用 setInterval 写轮播图的新手,还是天天和帧率、卡顿、掉帧打交道的资深开发,应该都能从这里挖到点能直接抄走的东西。核心就一件事:搞清楚 rAF 为什么能成为浏览器动画和渲染调度的标准姿势,以及它到底怎么帮我们在 js 性能优化这条路上少走弯路。

1. 从动画卡顿说起:为什么我们需要 requestAnimationFrame

我第一次认真研究 rAF,是因为一个特别具体的线上问题:一个活动页的抽奖转盘,用 setInterval 每 16 毫秒转一次,在低端安卓机上卡得像 PPT,转盘在转的过程中还会突然“抽搐”一下。用户投诉、运营追着问,那一周我把动画这块从头到尾翻了一遍,也正是那次让我彻底换掉了 setInterval。所以讲 rAF 之前,我得先把“为什么老办法不行”这件事说透,你才能真正理解它解决了什么。

1.1 定时器动画的先天缺陷,藏在时间片里

setInterval 和 setTimeout 的调度,本质上是把任务扔进浏览器的宏任务队列,等主线程空了再来执行。问题就在这儿:动画需要一个稳定的执行节奏,而定时器给不了这个保证。你写setInterval(fn, 16),期待的是一秒 60 帧,但实际运行中,主线程可能正忙着解析一段大 JSON、执行一个复杂计算,或者刚触发了大量的样式重排。这些活儿一占住线程,你的定时器回调就只能排队等着,等它终于被执行时,浏览器可能已经错过好几帧了。

更麻烦的是,定时器的时间间隔和浏览器真正刷新的节奏根本对不齐。显示器有自己的刷新率,常见的是 60Hz,也就是每 16.67 毫秒刷新一次画面,有些高刷屏是 90Hz、120Hz 甚至更高。你设的 16 毫秒,和硬件的 16.67 毫秒永远在错位,错位积累起来就会出现“一帧里画了两次、下一帧什么都没画”的情况,视觉上就是抖动、跳帧。我在低端机上实测过,一个用 setInterval 定时的进度条,帧间隔的波动能到 8ms 到 40ms 之间来回跳,那画面看着能舒服才怪。

提示:setTimeout 还有个“最小延迟”的问题,嵌套层级深了之后浏览器会强制把延迟拉到 4ms 以上,这直接让高频定时器动画变得不可控。

所以定时器动画的本质缺陷,是它试图用一个“逻辑时间”去驱动一个“物理刷新”的动作,而这两者压根不同步。你压不住帧率,也躲不开主线程的阻塞,卡顿是必然的。

1.2 requestAnimationFrame 的设计初衷,是跟浏览器渲染管线握手

rAF 的思路完全不一样。它不问“你希望多久执行一次”,而是说“浏览器,你下一次要重绘之前,顺便把我这个回调也执行了吧”。这个“恰好在下一次重绘之前”就是它的精髓所在。浏览器的渲染是一帧一帧走的,每一帧大致会经历这么几个阶段:处理输入事件、执行定时器和 microtask、执行 rAF 回调、计算样式、布局、绘制、合成。rAF 回调被安排在了“计算样式和布局”之前,也就是说,你在回调里改的 DOM 和样式,会正好被这一帧的渲染流程消化掉,不会白白多渲染一次。

这就带来了两个直接好处。第一,帧率天然对齐:60Hz 屏幕上它就是一秒 60 次,120Hz 屏幕上它就是 120 次,浏览器会自动按当前屏幕的刷新率来调度,你完全不用操心那个恼人的 16.67。第二,避免无效渲染:页面在后台标签页、或者用户切到别的窗口时,浏览器会主动把 rAF 回调暂停,省电省 CPU;而 setInterval 依然在后台傻乎乎地跑,白白消耗资源。我之前做过一个数据看板,切到后台再切回来,用定时器的版本累计跑了上千次无用回调,换成 rAF 之后后台直接归零。

你可以把 rAF 理解成“预约制”:你不是每隔一段时间硬闯进来说“我要画东西”,而是提前跟浏览器打好招呼“你下次作画前叫我一声”,然后由浏览器统一安排。这种和渲染管线握手的机制,是它区别于所有定时器方案的根本。

2. requestAnimationFrame 的核心机制拆解

光知道“它更好”还不够,要用得顺手,得把它的运行机制摸清楚。这一章我想讲三个东西:回调到底在什么时候被调用、它和定时器到底差多少、以及那个容易被忽略的回调参数时间戳该怎么用。这几点搞明白了,你写出来的动画代码质量会直接上一个台阶。

2.1 帧率、垂直同步与回调执行时机

先说“垂直同步”这个概念。显示器刷新画面不是随机的,它有一个固定的节奏,电子束从上到下扫描一遍(现代屏幕是逐行刷新),这个动作叫一次刷新。显卡往屏幕上写新画面的时候,如果写早了或者写晚了,导致写到一半屏幕开始刷新,就会出现画面撕裂——上半部分是旧画面、下半部分是新画面,看着像被横着切了一刀。垂直同步就是为了解决这个,让绘制动作必须等到一次刷新结束、下一次刷新开始之前完成,保证整屏一致。

rAF 的回调时机就卡在这个位置:它保证回调在浏览器准备好进行下一次重绘时被调用。如果这一帧主线程太忙,浏览器判断来不及完成渲染,它就可能跳过这次渲染,你的回调也会被推迟到下一个真正渲染的帧。这一点特别重要——它意味着 rAF 会“自适应”,忙的时候少画一帧保证不撕裂,闲的时候按满帧率跑。我拿一个持续运行的 rAF 循环在性能面板里看过,帧率会随着页面负载在 60、45、30 之间浮动,但画面始终是连贯的,没有那种定时器动画里突兀的一跳。

还有一个细节:一帧内多次调用 requestAnimationFrame,浏览器并不会给你安排多个回调,它会合并成同一个回调队列,在下一个渲染帧统一执行。这个特性后面在“多任务合并”里我会详细用它做优化。

2.2 requestAnimationFrame 与 setInterval 对比实测

空口说无凭,我整理了一张对比表,这些结论基本都来自我在中低端安卓机和主流桌面浏览器上的实测:

对比维度setInterval / setTimeoutrequestAnimationFrame
执行节奏固定时间间隔,与屏幕无关跟随屏幕刷新率自适应
帧率对齐容易错位,产生抖帧天然对齐,画面连贯
后台表现后台标签页仍在运行,耗电自动暂停,恢复时继续
主线程繁忙时回调堆积,动画跳跃自动丢帧,保证连贯
是否触发多余重绘可能触发无效渲染与渲染管线协同,无浪费
高刷新率支持需手动改间隔,适配麻烦自动跟随 120Hz 等
精准时间控制时间间隔不准回调带高精度时间戳

从表里能看出来,定时器唯一还能打的场景,是那些“必须按时执行”的网络心跳、轮询之类的逻辑任务。凡是和视觉动画、UI 更新相关的,rAF 几乎是全面占优。我现在的原则很简单:只要能画在屏幕上的东西,就用 rAF;只有不涉及画面的定时逻辑,才考虑定时器。

2.3 回调时间戳:被低估的高精度计时器

很多人写 rAF 的时候都是requestAnimationFrame(function() { ... }),完全无视了回调里那个参数。其实浏览器传给回调的这个时间戳非常好用,它是一个高精度的 DOMHighResTimeStamp,单位是毫秒,精度可以到微秒级(取决于浏览器的安全策略),而且它是相对于页面导航开始时间的单调递增值,不受系统时间调整影响。

它最大的价值是帮你把“用了多少时间”这件事算准。举个最常见的场景,一个物体从 A 点移动到 B 点,你不应该用“每次移动固定距离”来做,那样帧率一变速度就变了。正确做法是用时间戳算差值:

let start = null; const duration = 1000; // 动画持续 1 秒 function step(timestamp) { if (start === null) start = timestamp; const elapsed = timestamp - start; const progress = Math.min(elapsed / duration, 1); // 用 progress 去算位置,1 秒后 progress 到 1 box.style.transform = `translateX(${progress * 300}px)`; if (progress < 1) { requestAnimationFrame(step); } } requestAnimationFrame(step);

这样写不管中间掉了几帧、帧率是 60 还是 30,物体从 A 到 B 永远耗时一秒,视觉速度是稳定的。这就是所谓的“基于时间而非基于帧”的动画,是做流畅动画的基石。我以前用固定步长写动画,在 120Hz 屏幕上速度直接翻倍,闹过笑话,后来全部改成时间驱动才彻底解决。

提示:timestamp 是单调递增的,但不要让它在长时间停留后继续累加(比如后台切回来),因为切后台期间它可能不再更新,恢复后可能出现一个巨大的时间差,记得用 Math.min 或重置 start 来兜底。

3. 实战:用 requestAnimationFrame 重构一个卡顿的动画

理论讲够了,来点真东西。这一章我拿一个真实重构过的进度条动画做例子,从反面教材到正面解法,中间还会讲怎么封装一个通用调度器,以及怎么用合并技巧一次 rAF 处理多个动画任务。这套东西我在好几个项目里反复用过,直接搬就能生效。

3.1 反面案例:setInterval 实现的滚动进度条

先看原始代码,这是我早期写的一个页面顶部滚动进度条,逻辑是监听滚动、更新宽度,同时用定时器做平滑过渡:

// 反面教材:定时器驱动 + 每帧读布局 let progress = 0; setInterval(function () { const scrollTop = document.documentElement.scrollTop; // 读布局 const total = document.body.scrollHeight - window.innerHeight; progress = scrollTop / total; bar.style.width = (progress * 100) + '%'; // 写布局 }, 16);

这段代码有三个致命问题。第一,每 16 毫秒强制读取 scrollTop,这会触发浏览器的布局计算,如果在读之后又立刻写样式,就形成了“读写交替”,导致布局抖动,一帧内可能触发多次重排。第二,用 setInterval 完全不看渲染时机,滚动过程中主线程本来就忙,回调更容易堆叠。第三,.width这个属性的变化会触发重排加重绘,开销比 transform 大得多。实测在低端机上,滚动时帧率掉到 20 出头,进度条明显一卡一卡的。

注意:在滚动、动画这种高频场景里,任何一次“读布局后又写样式”的操作都可能引发强制同步布局,这是性能杀手,比单纯的重绘严重得多。

3.2 正面对比:用 requestAnimationFrame 重写

同样的功能,用 rAF 加读写分离重写之后,体验完全不一样:

let ticking = false; const bar = document.querySelector('.progress-bar'); function updateProgress() { const scrollTop = document.documentElement.scrollTop; const total = document.body.scrollHeight - window.innerHeight; const ratio = total > 0 ? scrollTop / total : 0; // 用 transform 替代 width,避开重排 bar.style.transform = `scaleX(${ratio})`; ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { // 只登记一次 rAF,滚动再频繁也只在下一帧处理一次 requestAnimationFrame(updateProgress); ticking = true; } }, { passive: true });

改动看着不大,但每一步都有讲究。ticking标志位的作用是节流,滚动事件可能一秒触发上百次,但我们只在下一帧真正处理一次,中间的全部合并掉。{ passive: true }告诉浏览器这个监听器不会调用 preventDefault,浏览器就能立刻滚动、不用等回调执行完,移动端上这个细节能明显提升滚动手感。而scaleX走的是合成层,不触发重排重绘,性能比改 width 好一大截。我实测这套改下来,滚动帧率稳定在 58 到 60 之间,几乎满帧。

同样的思路可以用在所有高频事件上:resize、mousemove、拖拽,都是“事件里只登记、rAF 里统一处理”的模式。

3.3 封装一个通用的动画调度器

项目里动画一多,每个都写一套 rAF 递归太乱,我干脆封装了一个轻量的调度器,把所有动画任务统一到一次 rAF 循环里:

const Scheduler = (function () { const tasks = new Set(); let running = false; function loop(timestamp) { // 先统一执行所有任务,读取阶段 tasks.forEach(function (task) { if (task.active) { task.fn(timestamp); } else { tasks.delete(task); } }); if (tasks.size > 0) { requestAnimationFrame(loop); } else { running = false; } } function add(fn) { const task = { fn: fn, active: true }; tasks.add(task); if (!running) { running = true; requestAnimationFrame(loop); } return task; } function remove(task) { if (task) task.active = false; } return { add: add, remove: remove }; })();

这个调度器的好处很明显:不管页面有多少个动画在跑,全局只有一次 rAF 循环,省去了反复注册的开销;用 Set 管理任务,增删都是 O(1);任务执行完自动清理,不会留下野回调。我用它同时驱动过页面上的六个元素动画,性能面板里 rAF 回调次数反而比之前每个动画各自递归还少。用起来也简单:

const t = Scheduler.add(function (ts) { // 你的动画逻辑 if (done) Scheduler.remove(t); });

3.4 多任务合并与节流配合

除了调度器,还有一个我常用的合并技巧:把“同一帧内需要读的量”集中读、“需要写的量”集中写。浏览器在一帧里,如果你先写后读,它会为了给你正确的读值而强制重排。正确的做法是一次性把所有读操作做完,再统一写:

Scheduler.add(function () { // 读取阶段:集中读,避免中间插写 const a = el1.getBoundingClientRect(); const b = el2.offsetWidth; // 写入阶段:集中写 el1.style.transform = `translateX(${a.width}px)`; el2.style.width = b + 'px'; });

这个“读写分离”的思维,配合 rAF 使用,能直接把每帧的重排次数从几次压到零次。我在一个拖拽排序的列表里用过,拖动时的卡顿感基本消失。这部分的坑还有一个:如果读取的值依赖刚才写入的样式,那你没法分离,只能认栽多一次重排,但这种情况其实可以通过维护一份 JS 侧的状态缓存来规避,别什么都去问 DOM。

4. 性能优化的深层技巧:rAF 只是起点

rAF 能解决“何时画”的问题,但“画什么、怎么画”同样决定最终性能。这一章我想把和 rAF 配合的几项关键优化展开讲:怎么避免布局抖动、怎么利用合成层、以及怎么和现代观察器 API 打配合。这些内容算是把 js 性能优化从“会写”拉到“写得好”的关键。

4.1 布局抖动:rAF 也救不了的强制同步布局

先说个容易踩的坑:rAF 保证了你回调的时机,但它管不了你回调里干了什么。如果你在 rAF 回调里交替读写布局,一样会触发强制同步布局,rAF 也救不了你。所谓强制同步布局,就是你改了样式之后立刻去读一个布局属性(比如 offsetHeight、getBoundingClientRect、scrollTop),浏览器为了给你准确的值,被迫马上把之前所有的样式变更同步计算一遍,把本该在渲染阶段做的事提前到现在做。一次两次还好,一帧里来个几十次,帧率立刻崩。

我排查这类问题的办法很简单,打开性能面板录一段,看有没有那种特别窄但特别密集的“Recalculate Style”或“Layout”块。如果有,就说明在反复强制重排。解决套路还是老几样:能缓存的布局值就缓存,别每帧去读;读写一定要分离;实在要读,挪到这一帧的读取阶段统一读。我见过一个动画最夸张,一帧里读了 50 次 scrollTop,把 rAF 用出了定时器的卡顿感,改完之后帧率从 30 直接回到 60。

注意:transform 和 opacity 之外的大部分样式变更都会触发重排或重绘,能用 transform 就绝不用 top/left/width/height 做动画。

4.2 合成层与 GPU 加速,让动画真正丝滑

想把动画做到极致的丝滑,得理解合成层。浏览器渲染最后一步是合成(Composite),把各个图层像 PS 图层一样拼到一起。如果你能把需要频繁变化的元素提升为一个独立的合成层,那它的变化就只在 GPU 层面完成,完全不需要 CPU 重排重绘,性能自然飞起。最常用的提升手段是 transform 和 opacity,它们本来就能被 GPU 直接处理。必要的时候,可以加will-change: transform提前告诉浏览器“这元素要动,先给它单独开个层”。

但这里有个大坑要提醒你:别滥用 will-change。每个合成层都要占用 GPU 显存,你给页面上几十个元素都加上 will-change,显存分分钟被吃满,反而更卡。我一般只在动画即将开始时加、结束时移除,比如鼠标悬停的卡片、正在拖拽的元素。我见过有人图省事给整页元素全加上 will-change,结果低端机直接白屏,得不偿失。记住一句话:合成层是稀缺资源,用在该用的地方。

4.3 与 IntersectionObserver、ResizeObserver 配合

现代浏览器给了一组“观察器”API,它们和 rAF 是绝配。比如你有一堆元素要在进入视口时才播放动画,用 scroll 事件去判断每个元素位置,本身就慢,还会和 rAF 抢资源。换成 IntersectionObserver,浏览器在合适的时机回调告诉你某个元素进出了视口,你只在真正要动的地方启动 rAF 动画,其余时间完全零开销。

const observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { startAnimation(entry.target); // 进入视口才开始 rAF 动画 observer.unobserve(entry.target); } }); }, { threshold: 0.1 }); document.querySelectorAll('.animate-on-scroll').forEach(function (el) { observer.observe(el); });

这套组合我在长列表页里用过:一屏元素全用 rAF 跑动画,滚动直接卡死;改成 IntersectionObserver 按需启动,滚动全程保持满帧。ResizeObserver 也是同理,元素尺寸变化时才触发,比监听 window resize 精确得多,也不会像 resize 那样疯狂触发。这几个 API 的共同思路,都是“把该省的省掉,把资源留给真正需要动的那一刻”,这和大方向的 js 性能优化妥妥一路。

5. 常见坑与排查技巧实录

最后这一章我掏心窝子聊坑。这些是我和团队这几年真金白银踩出来的,文档里基本不会写,但每一个都能让你少熬几个通宵。我把它们整理成速查表和几条独家避坑经验,你照着排查能省不少事。

5.1 页面隐藏时 rAF 停止,这个“特性”也能变坑

rAF 在后台标签页自动暂停,这是好事,省电省资源。但它也会带来问题:如果你的动画逻辑依赖 rAF 的持续累加来推进状态,切后台再切回来,动画会“原地卡住”然后突然恢复,甚至因为时间戳跳变出现一帧移动超远的情况。我在一个在线播放器里就遇到过,切后台一会儿回来,进度条猛地窜一大截。解决办法是记录document.visibilityState,切回来时重置计时起点:

let lastTime = null; function loop(ts) { if (lastTime === null) { lastTime = ts; requestAnimationFrame(loop); return; } const delta = ts - lastTime; // 如果单帧间隔超过 100ms,说明可能刚从后台回来,跳过这次累加 if (delta > 100) { lastTime = ts; requestAnimationFrame(loop); return; } lastTime = ts; // 正常用 delta 推进动画 requestAnimationFrame(loop); }

这个“超过阈值就跳过”的兜底逻辑很实用,能挡住大部分后台切换、断点调试导致的时间跳变。

5.2 忘记取消 rAF,内存泄漏悄无声息

第二个高频坑:用了 rAF 却没在组件卸载时取消。你在一个弹窗里挂了 rAF 循环,弹窗关掉了,但循环还在跑,因为它根本没被清理。这种泄漏特别隐蔽,页面上看不出什么,但 CPU 一直在被消耗。规范做法是保存 rAF 返回的 id,在清理时机调用 cancelAnimationFrame:

let rafId = null; function loop(ts) { // ... rafId = requestAnimationFrame(loop); } function cleanup() { if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } } // 比如组件卸载、弹窗关闭时调用 cleanup

用前面的调度器模式其实也能规避这个问题,因为任务执行完会从 Set 里自动删掉,但手动写递归的话,取消这一步永远别忘了。

5.3 常见问题速查表

下面这张表是我自己整理的“rAF 排查手册”,遇到问题先对着看一遍,能解决八成:

现象可能原因排查与解决
动画抖动、跳帧混用定时器,或帧率错位统一改用 rAF,用时间戳驱动
切后台回来画面突变时间戳跳变未处理加 delta 阈值判断并跳过
动画卡顿但 rAF 正常回调里强制同步布局读写分离,缓存布局值
页面越用越卡rAF 未取消导致泄漏保存 id,卸载时 cancel
低端机白屏will-change 滥用吃显存按需添加,动画结束即移除
高刷屏速度变快用固定步长而非时间改为基于时间戳的进度计算
滚动时进度条卡每帧读 scrollTop缓存、节流、读写分离

5.4 几条独家避坑经验

最后再补几条表格里塞不下的经验。第一,调试动画时别用 console.log 疯狂打印,日志本身就是开销,会掩盖真实的性能表现,用 performance.now() 记录时间点更靠谱。第二,别迷信“帧率越高越好”,我把一个动画从 60 帧优化到 120 帧,用户根本没感觉,反而多耗电,能稳定 60 帧就够了。第三,用 rAF 做节流比时间戳节流更贴合渲染,凡是和画面更新相关的节流,优先考虑“事件里登记、rAF 里执行”这个模式,比如 mousemove 拖拽,手感会明显更好。

我个人在实际项目里的体会是,rAF 这东西“会用”和“用好”之间隔着一整条性能优化的路。它本身只有两个 API,但真正决定动画是否丝滑的,是你有没有把渲染时机、读写顺序、合成层、任务合并这些细节串成一条线。我现在的习惯是,任何一个要上生产环境的动画,先过一遍这套检查:是不是用 rAF 驱动、时间戳算的进度、有没有读写分离、rAF 有没有取消、will-change 加得克不克制。这几步走完,基本不会翻车。如果你手上正好有个卡顿的动画要救,不妨从“把 setInterval 换成 rAF”这一步开始,往往立竿见影。

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

OFDM系统MATLAB仿真全解析:从IFFT到BER曲线验证

简介&#xff1a;面向通信工程学习者与MATLAB初学者的OFDM&#xff08;正交频分复用&#xff09;算法实现包&#xff0c;聚焦多载波调制核心原理&#xff0c;覆盖从信道分割、子载波正交化到收发链路的完整仿真流程。OFDM是4G/5G、Wi-Fi等现代系统的核心技术&#xff0c;理解其…

作者头像 李华
网站建设 2026/9/17 3:59:53

Spring Boot校园服务平台源码解析与二次开发实战指南

Spring Boot学生校园服务生活集合平台这类项目&#xff0c;每年毕业季都能在源码站刷到好几页同款。标题里挂着的“附源码67568”说明这套东西已经流传得很广了&#xff0c;也侧面印证了校园服务平台在选题界的常青树地位——它业务场景真实、功能边界清晰、技术栈又刚好卡在Ja…

作者头像 李华
网站建设 2026/9/17 3:57:38

计算机网络第一章复习指南:时延计算与分层模型高频考点解析

说起计算机网络复习&#xff0c;谢希仁老师这本第八版教材几乎是无数人的入门标配。很多人把第一章“概述”当成纯记忆章&#xff0c;觉得背背概念就过去了&#xff0c;但真正考试和考研408里&#xff0c;第一章的失分点恰恰最多。第一章看起来是“常识”&#xff0c;实际上埋了…

作者头像 李华
网站建设 2026/9/17 3:54:59

书匠策AI:用“学术考古”重塑文献综述写作流程

写文献综述这事儿&#xff0c;说到底就是一场体力活加脑力活的双重折磨。我见过太多同学&#xff0c;开题时雄赳赳气昂昂&#xff0c;打开知网Web of Science一顿猛下&#xff0c;存了两百多篇PDF&#xff0c;觉得自己能写出一部学术巨著。结果真开始动笔&#xff0c;对着满屏密…

作者头像 李华
网站建设 2026/9/17 3:54:56

函数定义与参数设计:从代码复用到可维护架构

最近后台收到好几条私信&#xff0c;都是问同一个问题&#xff1a;“函数到底怎么定义才不是瞎写&#xff1f;参数啥时候该放哪&#xff1f;”仔细一看&#xff0c;都是刷到函数这一章卡住了。函数这个东西确实很奇妙&#xff0c;代码量少的时候你觉得它多余&#xff0c;代码量…

作者头像 李华