news 2026/9/30 5:47:33

手写 JS 动画函数:requestAnimationFrame 与缓动优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 JS 动画函数:requestAnimationFrame 与缓动优化

1. 手写动画函数这件事,到底还有没有必要

1.1 从一次"CSS 动画不听话"的真实场景说起

前两年做过一个数据看板,里面有根横向进度条,需求是:随着数据分批返回,进度条一点点往前爬,中途如果某批数据校验失败,进度条要往回退一小段,然后继续往前。我一开始用的是 CSStransition: width .4s linear,改一次style.width就等浏览器自己补间。结果很快翻车:连续快速改目标宽度时,浏览器每次都从"当前渲染值"重新起算过渡,视觉上会出现速度忽快忽慢的锯齿感;更麻烦的是"回退"这个动作,transition只知道从 A 到 B,它没法告诉我"现在实际走到哪了",我想在中途插一个短暂的停顿、或者根据当前进度调整回退距离,根本拿不到那个中间值。

后来我把这段逻辑换成手写的动画函数,逐帧计算并写入宽度,问题基本一次性解决。原因很朴素:CSS 动画是声明式的,浏览器替你决定中间过程;JS 动画函数是命令式的,每一帧的数值都握在你手里。需要读取中间状态、需要中途改变方向、需要和业务数据联动时,命令式的控制力就是刚需。这就是我至今仍在项目里保留一个自研动画函数的核心原因,它不是替代 CSS 动画,而是补上那些 CSS 覆盖不到的角落。

1.2 四种主流方案横向摆开对比

在动手写之前,先把可选路径理清楚,不然容易陷入"造轮子还是用轮子"的自我怀疑。下面这张表是我自己踩过坑之后整理的,判断维度就三个:可控粒度、上手成本、体积开销。

方案可控粒度上手成本体积/依赖适合场景
CSS transition/animation低,只能改起止和目标极低零悬停、进入退出、固定节奏动效
Web Animations API中,可读写currentTime、可暂停中零(原生)需要暂停/倍速但不需要复杂插值的场景
requestAnimationFrame 手写高,逐帧数值全掌控中高零数据联动、可中断、需要中间值
第三方动画库高,功能齐全低几十 KB 起复杂时间轴、SVG 形变、物理弹簧

看这张表能得出一个很直接的取舍逻辑:如果你的需求是"元素出现时淡入",那用 CSS 就完事了,没必要上 JS;如果是"三个元素按时间轴错开进场,中间还得插入一段路径动画",直接上成熟库,自己写纯属给自己找麻烦。真正介于两者之间的那类需求——动的东西是数据、状态随时可能被外部事件打断——才是自研动画函数的主场。

1.3 自研动画函数的边界感

有一点必须说清楚,我写这个函数不是为了炫技,也不是为了"不依赖第三方"。它的定位就是一个几十行的工具模块,职责单一:给定起点和目标,在指定时长内平滑过渡,且允许我随时拿到中间值、随时叫停。凡是超出这个职责的需求,一律不硬扛。比如复杂的缓动曲线叠加、多段串联的时间轴、需要和物理引擎结合的回弹效果,我会老老实实引库,因为那些东西的复杂度不在插值本身,而在调度和边界处理上,自己写大概率会写出一堆补丁。

明确边界还有一个好处:代码量小,可读性高,团队成员接手时几分钟就能看完,不用去啃一堆配置项。我这套函数加上注释也就一百多行,包含缓动表、主循环、控制句柄三部分,任何有 JS 基础的人半小时内能读懂并改。这个门槛对于长期维护来说是极重要的隐性成本。

2. 动画的本质:三个变量和一个循环

2.1 时间、进度、插值这三件事

抛开所有花哨的效果,任何补间动画都可以归纳成一个式子。设动画总时长为d(毫秒),当前已经过去的时间为t,那么进度p = t / d,取值范围被限制在 0 到 1 之间。有了进度,最终值就是value = from + (to - from) * ease(p)。这里ease(p)是缓动函数,接收 0 到 1 的线性进度,吐出一个经过映射的进度。如果ease是恒等函数,那得到的就是匀速直线运动。

这个式子的价值在于把动画彻底解耦了:时间归时间,映射归映射,赋值归赋值。主循环只负责推进t并算出p,缓动函数只负责把p扭成想要的节奏,最后一步赋值才去碰真实 DOM。三个环节互不干扰,换缓动曲线不用改主循环,换渲染目标不用改缓动。我见过不少同学把这三者揉在一个函数里,结果想给另一个元素复用同一条曲线时只能复制粘贴,改一个 bug 要改五个地方。

注意:t千万不要用"帧数 × 每帧固定时长"去累加。屏幕刷新率有 60Hz、90Hz、120Hz 之分,某些设备在低电量时还会动态掉帧,用帧数累加会让同一段动画在高刷屏上飞快、在低刷屏上变慢。

2.2 缓动函数的直觉与几条常用曲线

缓动函数听起来玄,其实就是在回答一个问题:动画是"慢起快冲""快冲慢停",还是"两头慢中间快"。最常用的四条:easeInQuad是p * p,起步肉,越走越快;easeOutQuad是p * (2 - p),起步猛,结尾柔;easeInOutQuad是两段拼接,两头都软;easeOutCubic是1 - Math.pow(1 - p, 3),减速感比 quad 更明显,做元素"滑入停稳"特别顺手。

生活里最能对应上的是刹车。车子从高速刹停的过程接近easeOut——一开始减速明显,快停下时慢慢平移到位;而从静止起步接近easeIn。这也解释了为什么界面动效里easeOut系列用得最多:用户注意力集中在"结果落点"上,减速收尾能让眼睛更容易捕捉到最终位置。反过来,如果做一个元素"弹射出去"的效果,用easeIn才有蓄力感。

写缓动表我习惯用一个对象来装,取的时候直接按名字拿:

const Easing = { linear: p => p, easeInQuad: p => p * p, easeOutQuad: p => p * (2 - p), easeInOutQuad: p => p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p, easeOutCubic: p => 1 - Math.pow(1 - p, 3), easeOutBack: p => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(p - 1, 3) + c1 * Math.pow(p - 1, 2); } };

用对象而不是switch的理由很实际:调用方传一个字符串就能指定曲线,扩展新曲线只需要往对象里加一个键,主循环一行都不动。easeOutBack那个带点回弹的曲线,参数1.70158是经典常数,改动它就能调节回弹幅度,超过大概 3 就会显得很浮夸。

2.3 requestAnimationFrame 的两个典型陷阱

requestAnimationFrame是浏览器给动画准备的专用调度器,它会在下一次重绘之前调用回调,并把一个高精度时间戳作为参数传进来。用它而不是setInterval的原因大家基本都知道:和刷新节奏对齐,后台标签页自动暂停,省电。但有两个坑我见得太多了。

第一个坑是用自增计数当时间。setInterval时代留下的习惯,改成rAF之后很多人还是写一个count++当帧号。前面说过这样在异构刷新率下会出问题。正确做法是第一次调用记录startTime,之后每次用now - startTime得到真实耗时。

第二个坑是后台恢复时的大跳跃。页面切到后台,rAF停止触发,切回来时第一个时间戳会比上一次大出一大截,如果不做处理,动画会瞬间跳到终点。稳妥的做法是把每次的时间增量夹在一个上限内,或者干脆在恢复时重置起始时间:

const MAX_DELTA = 64; // 毫秒,约等于 15fps let last = performance.now(); function loop(now) { const delta = Math.min(now - last, MAX_DELTA); last = now; // 使用 delta 推进逻辑,而不是直接使用 now }

用增量推进还有个额外好处:如果你的动画逻辑要支持暂停,暂停期间不累加即可,恢复时不会出现"暂停了三秒结果瞬间跑完"的尴尬。

3. 一个能直接抄走的基础动画函数

3.1 最小可用版本,先跑起来再谈优雅

先写一个只支持单个数值变化的版本,用来验证思路。参数依次是起点、终点、时长、每帧回调、缓动曲线:

function animateNumber(from, to, duration, onUpdate, easing = Easing.easeOutQuad) { const start = performance.now(); let rafId = 0; function step(now) { const elapsed = now - start; const p = Math.min(elapsed / duration, 1); const eased = easing(p); onUpdate(from + (to - from) * eased); if (p < 1) { rafId = requestAnimationFrame(step); } } rafId = requestAnimationFrame(step); return () => cancelAnimationFrame(rafId); }

这段代码里有个细节值得说:p用了Math.min(elapsed / duration, 1)做钳制,而不是直接比较elapsed >= duration来跳出。这样即使某一帧的elapsed略微超过时长,进度也只会停在 1,onUpdate收到的终值永远精确等于to,不会出现"差一点点没到位"的情况。这个精度问题在时长很短(比如 150ms)的动画里特别明显,我最初就吃过亏,进度条永远缺那么零点几个像素。

3.2 升级成多属性动画,一次动一串样式

单值版本只适合计数之类的小场景,真正做界面动效,通常要一次改好几个属性。这时候思路要从"数值"升级到"数值集合":先算出起点的快照,再把终值记下来,每一帧计算每个属性的当前值并写入。

function animateTo(target, toMap, duration, options = {}) { const { easing = Easing.easeOutQuad, onUpdate, onComplete } = options; const fromMap = {}; const keys = Object.keys(toMap); keys.forEach(k => { const current = parseFloat(getComputedStyle(target)[k]) || 0; fromMap[k] = current; target.style[k] = ''; }); const start = performance.now(); let rafId = 0; let cancelled = false; function step(now) { const elapsed = now - start; const p = Math.min(elapsed / duration, 1); const e = easing(p); keys.forEach(k => { const value = fromMap[k] + (toMap[k] - fromMap[k]) * e; target.style[k] = typeof toMap[k] === 'number' ? `${value}px` : value; }); if (onUpdate) onUpdate(p); if (p < 1) { rafId = requestAnimationFrame(step); } else if (onComplete) { onComplete(); } } rafId = requestAnimationFrame(step); return { cancel() { cancelled = true; cancelAnimationFrame(rafId); } }; }

读起点值这里用了getComputedStyle而不是直接读target.style.width,这是个关键选择。内联样式如果没设置过,style.width是空字符串,解析出来是NaN;而getComputedStyle会返回浏览器实际计算后的值,比如"120px"。拿到之后再立刻清掉内联样式,是为了让后续的每一帧写入成为唯一的数据来源,避免旧值干扰。如果你不放心,可以在调用前手动传入起点,接口上留一个fromMap覆盖参数就够了。

3.3 补上暂停、取消与完成回调

能跑起来之后,真正决定这个函数好不好用的,是控制能力。业务上最常见的情况是:动画还没跑完,用户切换了 Tab,或者数据重新加载了,这时候必须能干净地把动画停掉,否则它会在后台继续改 DOM,轻则浪费性能,重则把新数据渲染出来的样式覆盖掉。

上面的例子已经返回了一个带cancel的句柄,我通常会把它扩展成完整的一套:

function createTween({ duration, easing = Easing.easeOutQuad, onUpdate, onComplete }) { let rafId = 0; let startTime = 0; let elapsedBeforePause = 0; let state = 'idle'; // idle | running | paused | done function tick(now) { if (state !== 'running') return; const elapsed = elapsedBeforePause + (now - startTime); const p = Math.min(elapsed / duration, 1); onUpdate(easing(p), p); if (p < 1) { rafId = requestAnimationFrame(tick); } else { state = 'done'; onComplete && onComplete(); } } return { start() { if (state === 'running' || state === 'done') return; state = 'running'; startTime = performance.now(); rafId = requestAnimationFrame(tick); }, pause() { if (state !== 'running') return; state = 'paused'; elapsedBeforePause += performance.now() - startTime; cancelAnimationFrame(rafId); }, resume() { if (state !== 'paused') return; this.start(); }, cancel() { state = 'idle'; elapsedBeforePause = 0; cancelAnimationFrame(rafId); } }; }

暂停的实现要点在于把"已经跑过的时间"存下来。暂停时把elapsedBeforePause累加上去,恢复时重新取一个startTime,两者相加就是新的真实耗时。这套写法我用了很久,稳得很,也不会出现恢复后进度跳变的问题。有个小提醒:cancel之后不要再复用同一个实例,直接重新创建一个,状态更干净。

3.4 缓动表的组织与扩展方式

前面提过缓动函数用对象管理,这里补一句扩展时的小技巧。如果你想让调用方传字符串之外还能传自定义函数,可以在取值处做一次判断:

function resolveEasing(easing) { if (typeof easing === 'function') return easing; return Easing[easing] || Easing.easeOutQuad; // 兜底,避免传错名字直接崩 }

兜底这一行看着不起眼,救过我好几次。团队协作时,有人传了"easeout"这种大小写不一致的名字,Easing["easeout"]是undefined,如果不兜底,easing(p)会直接抛错,整个动画链路断掉,而报错信息还很不直观。加个默认值之后,最坏情况就是动效节奏和预期不一样,不至于功能全废。

4. 实际项目里的几个落地案例

4.1 元素位移加淡出的组合动画

这是最通用的一个:列表项被删除时,先向右滑出,同时透明度降到 0,动画结束后再从 DOM 里移除节点。如果用 CSS 做,移除时机得靠transitionend事件,而这个事件在某些中断场景下不一定触发,容易留下孤儿节点。用 JS 动画函数就直接在onComplete里删:

function slideOut(el) { const tween = createTween({ duration: 280, easing: 'easeInQuad', onUpdate(e) { const x = 0 + 60 * e; el.style.transform = `translateX(${x}px)`; el.style.opacity = String(1 - e); }, onComplete() { el.remove(); } }); tween.start(); }

这里刻意使用了transform和opacity两个属性,而不是left和opacity。原因在后面的性能章节会展开,简单说就是这两个属性可以交给合成线程处理,不触发重排,动起来更顺。另外注意opacity必须转成字符串,直接赋数字在部分老版本浏览器里会被忽略。

4.2 数字滚动计数,大屏项目里的常客

数字从 0 滚到目标值,几乎每个数据大屏都会有。看起来简单,实际有两个细节要处理:一是插值算出来的必然是浮点数,需要按场景决定是否取整;二是千分位格式化,如果每帧都重新做一次字符串拼接,几十个数字同时滚动会有明显开销。

function countUp(el, to, duration = 1200, decimals = 0) { const formatter = new Intl.NumberFormat('zh-CN', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); const tween = createTween({ duration, easing: 'easeOutCubic', onUpdate(e) { const value = to * e; el.textContent = formatter.format(Number(value.toFixed(decimals))); } }); tween.start(); }

用Intl.NumberFormat而不是手写toLocaleString的字符串替换,是因为实例可以复用,格式化逻辑只编译一次。实测下来,同屏十五个计数器同时滚动,用复用实例的方式基本不掉帧,而每帧新建Intl.NumberFormat的写法在低端设备上会明显卡顿。另外value.toFixed(decimals)之后再Number()一下,是为了避免出现"1,234.00"前面带上负零这种奇怪输出。

4.3 随滚动变化的进度指示条

页面顶部那根随着滚动往下走的进度条,很多人写成在scroll事件里直接算宽度并赋值。这个写法的问题在于scroll事件触发频率远高于屏幕刷新率,在一秒内可能触发上百次,每一次都改样式,等于把大量计算浪费在了根本不会渲染的帧上。

我的做法是把scroll事件降级为"只记录目标值",真正的计算和赋值放到rAF里:

let ticking = false; let targetProgress = 0; window.addEventListener('scroll', () => { const total = document.documentElement.scrollHeight - window.innerHeight; targetProgress = total > 0 ? window.scrollY / total : 0; if (!ticking) { ticking = true; requestAnimationFrame(flush); } }, { passive: true }); function flush() { bar.style.transform = `scaleX(${targetProgress})`; ticking = false; }

{ passive: true }这个选项一定要加上,它告诉浏览器"这个监听器不会调用preventDefault",浏览器就可以放心地在滚动线程里同步执行回调,不会因为等待主线程而产生滚动延迟。而用scaleX而不是width,则是因为缩放同样走合成层,改动成本更低。

4.4 视频元素旋转这类小需求

有个需求是让一个内嵌的视频做 90 度旋转播放(横屏素材竖屏展示)。最直接的写法是:

const v = document.querySelector('video'); v.style.rotate = '-90deg'; v.style.transformOrigin = 'center center';

一次性的旋转这么写没问题。但如果是要做一个"点击后缓缓转过 90 度"的过渡效果,style.rotate这种独立变换属性的过渡行为在不同浏览器里表现不完全一致,我更倾向于统一用transform,并且用动画函数逐帧写入角度值,这样节奏完全可控:

function rotateTo(el, fromDeg, toDeg, duration = 500) { const tween = createTween({ duration, easing: 'easeInOutQuad', onUpdate(e) { const deg = fromDeg + (toDeg - fromDeg) * e; el.style.transform = `rotate(${deg}deg)`; } }); tween.start(); }

要留意的坑是:如果元素上原本已经有transform(比如scale或者translate),单独写rotate会覆盖掉原有的变换。稳妥做法是把原有变换值先取出来,拼成完整的transform字符串再写入,或者改用transform的矩阵运算。另外旋转后的视觉尺寸会变大,容器如果没有预留空间可能会被裁切,这些都比动画本身更容易出问题。

5. 踩坑实录:动画不流畅到底是谁的问题

5.1 首帧跳变与抖动的三种成因

动画第一帧就"啪"地跳一下,几乎每个手写动画的人都遇到过。我总结下来主要是三个原因。第一种是起始时间基准不统一:from值是用getComputedStyle取的,但起始时间performance.now()是在之后才取的,中间隔了几毫秒的样式计算开销,导致第一帧的elapsed已经不为零,进度一下跳到百分之几。解决办法是把取值和记时间放在尽可能近的位置,或者干脆接受这点误差但保证第一帧进度从 0 开始。

第二种是样式读写交替。如果onUpdate里既写了样式又去读了布局属性(比如offsetWidth),浏览器会被迫立刻做一次重排,一帧内来回几次,逻辑上没跳,观感上却是一顿一顿的。第三种是缓动函数本身不平滑,比如easeOutBack在p很小时会输出负值,如果属性本身不支持负值(像opacity),就会出现颜色反转或直接无效。遇到回弹类曲线,记得对结果做一次范围钳制。

5.2 忘记取消的 rAF,内存泄漏的常见源头

单页应用里最常见的泄漏来源之一,就是组件卸载后rAF还在跑。循环里引用了 DOM 节点,节点本应被回收,却因为闭包还持有引用而一直留在内存里。更隐蔽的是,循环还在往一个已经脱离文档的节点写样式,控制台不报错,性能却持续消耗。

我的习惯是每个动画句柄都在组件的卸载钩子里取消,而且句柄统一存到一个数组里管理:

const tweens = []; function register(tween) { tweens.push(tween); return tween; } function clearAll() { tweens.forEach(t => t.cancel()); tweens.length = 0; } // 组件卸载时调用 clearAll()

这样即使某个动画叠了好几层,清理也是一次性的。有个细节:清理函数要在移除 DOM 之前调用,反过来顺序在高频卸载场景下会有短暂的一帧还在跑。

5.3 强制同步布局是怎么把帧率吃掉的

浏览器的渲染流程大致是:样式计算、布局、绘制、合成。布局这一步开销最大,而它会被"在修改样式之后立刻读取布局属性"这个动作强制提前执行,也就是所谓的强制同步布局。getBoundingClientRect、offsetTop、scrollHeight、getComputedStyle的某些值都会触发。

在动画循环里,这个问题的典型形态是:每帧都读取元素当前位置作为起点,然后又写入新位置。一读一写交替,浏览器每帧都被迫重排。正确的解法是把读取集中在动画开始前一次做完,循环里只做写入。这也是为什么我在animateTo里把所有起点值在启动时快照下来的原因,循环内部一次布局读取都没有。

5.4 常见问题速查表

现象可能原因排查方法处理手段
首帧跳变起始时间与取值时机不一致打印第一帧的p值取值与记时靠近,或强制首帧p = 0
动画中途卡一下循环内触发重排Performance 面板看 Layout 耗时读取前置,循环内只写样式
后台切回来瞬间跑完rAF恢复后时间戳大跳跃打印相邻两次now差值对增量做上限钳制
动画不执行元素display: none或未挂载检查节点是否在文档中先挂载再启动动画
结束后样式被还原内联样式被其他逻辑覆盖检查样式写入冲突动画结束后显式写入终值
低端机掉帧严重同时动画元素过多逐步减少元素数量对比分批启动,或改用合成属性

display: none那条特别值得单独提一句。元素不可见时,getComputedStyle拿到的尺寸全是 0,动画算出来的from就是 0,等元素显示出来再跑,起点完全不对。稳妥做法是动画启动前先确认节点可见,或者在隐藏状态下直接用预设的起点值。

6. 性能与工程化的收尾技巧

6.1 只动 transform 和 opacity 这条铁律

这条经验被说烂了,但真正理解它需要知道一点渲染管线。width、height、top、left、margin这些属性的改变会影响元素在文档流中的几何位置,浏览器必须重新计算布局,再重新绘制,最后才合成。而transform和opacity的改变发生在合成阶段,浏览器可以把元素提升为一个独立的合成层,直接交给 GPU 处理,主线程完全不参与。

实践上的选择很清晰:位移用translate,缩放用scale,旋转用rotate,淡入淡出用opacity。如果动画元素数量多,可以给容器加will-change: transform,但千万别给每个元素都加,滥用会让浏览器创建过多合成层,显存占用直线上升,反而更卡。我用will-change的原则是:只在动画即将开始前加上,结束后移除,让浏览器有明确的提示信号。

6.2 和框架协作时的边界处理

在 Vue 或 React 项目里直接改el.style是可以的,但要注意别和框架的渲染机制打架。核心原则是:框架管结构,动画函数管中间过程。也就是说,动画只修改那些框架不参与计算的样式属性,最终状态也由动画函数负责写到位,不要让框架在同一时刻也去更新同一份样式。

React 里我一般通过ref拿真实节点传给动画函数,绝不用useState去驱动每一帧。原因很直接:每帧setState会触发一次渲染和 diff,几百帧下来开销巨大,而且因为渲染是异步批处理的,视觉上还可能出现丢帧。Vue 里也是同理,动画期间避开响应式数据。另外在useEffect的清理函数里取消动画,是必做动作,前面已经说过原因。

6.3 把动画函数做成一个可复用模块

最后说说封装。我习惯把它做成一个独立的 ES Module,导出三样东西:Easing缓动表、createTween通用补间、以及几个高频场景的快捷函数(countUp、slideOut之类)。模块内部不做任何全局状态,每个动画实例独立,互不干扰。

export { Easing, createTween, countUp, slideOut };

还有个容易被忽略的工程细节:给动画加超时兜底。理论上rAF会一直跑下去直到进度到 1,但如果页面长时间处于不可见状态,某些浏览器可能不触发回调,动画就永远挂在那里。我一般在启动时用setTimeout设一个略长于时长的兜底,触发时强制把终值写到位并清理句柄。多这几行代码,能避免那些"偶尔卡在半路"的诡异问题。

我个人在多个项目里反复使用这套方案后的体会是,动画函数真正的价值不在于把元素动起来,而在于它给了你一个可以随时观测、随时干预的中间状态。当你需要根据当前进度做判断、需要让动画响应外部事件、需要在几个动画之间做同步时,这个中间状态就是一切灵活性的来源。很多看起来复杂的交互效果,拆到底层,不过是几个数字在几百毫秒里的连续变化而已。至于那个旋转视频的小需求,后来我干脆把它抽成了一个独立的工具方法,参数是元素、起始角度、目标角度和时长,调用一次就完事,比每次去拼transform字符串省心不少。

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

公共管理服务接入DeepSeek:场景盘点、架构选型到落地避坑全拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:46:17

ESP32联网实战:ESP-IDF实现WiFi获取天气与DHT11温湿度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:45:30

Lab色彩模式实战:从锐化校色到色彩管理的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:45:16

蛋白质亚细胞定位预测:AAIndex编码+CNN-BiLSTM实战指南

简介&#xff1a;本资源是一篇发表于《计算机应用》期刊的学术论文&#xff0c;面向生物信息学、计算生物学及人工智能交叉领域的研究者与高年级本科生/研究生&#xff0c;聚焦蛋白质亚细胞定位这一关键功能预测问题。论文提出基于堆栈式降噪自编码器&#xff08;SDAE&#xff…

作者头像 李华
网站建设 2026/9/30 5:45:06

用Python微调BERT做提取式摘要:论文代码核心解读与实战

简介&#xff1a;面向自然语言处理开发者与研究者&#xff0c;这份基于Python的BertSum实现围绕BERT微调完成抽取式摘要任务&#xff0c;覆盖从文本预处理、预训练模型加载、任务层构建到训练评估与后处理的完整流程。压缩包共36个文件&#xff0c;以20个py脚本为主&#xff0c…

作者头像 李华